mirror of
https://github.com/veops/oneterm.git
synced 2025-10-08 00:30:12 +08:00
feat(backend): add command template
This commit is contained in:
@@ -82,13 +82,15 @@ func (c *Controller) UpdateCommand(ctx *gin.Context) {
|
|||||||
// GetCommands godoc
|
// GetCommands godoc
|
||||||
//
|
//
|
||||||
// @Tags command
|
// @Tags command
|
||||||
// @Param page_index query int true "command id"
|
// @Param page_index query int true "page index"
|
||||||
// @Param page_size query int true "command id"
|
// @Param page_size query int true "page size"
|
||||||
// @Param search query string false "name or cmd"
|
// @Param search query string false "name or cmd"
|
||||||
// @Param id query int false "command id"
|
// @Param id query int false "command id"
|
||||||
// @Param ids query string false "command ids"
|
// @Param ids query string false "command ids"
|
||||||
// @Param name query string false "command name"
|
// @Param name query string false "command name"
|
||||||
// @Param enable query int false "command enable"
|
// @Param enable query int false "command enable"
|
||||||
|
// @Param category query string false "command category"
|
||||||
|
// @Param risk_level query int false "command risk level"
|
||||||
// @Param info query bool false "is info mode"
|
// @Param info query bool false "is info mode"
|
||||||
// @Param search query string false "name or cmd"
|
// @Param search query string false "name or cmd"
|
||||||
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Command}}
|
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Command}}
|
||||||
|
151
backend/internal/api/controller/command_template.go
Normal file
151
backend/internal/api/controller/command_template.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
|
||||||
|
"github.com/veops/oneterm/internal/model"
|
||||||
|
"github.com/veops/oneterm/internal/service"
|
||||||
|
"github.com/veops/oneterm/pkg/config"
|
||||||
|
pkgErrors "github.com/veops/oneterm/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
commandTemplateService = service.NewCommandTemplateService()
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateCommandTemplate godoc
|
||||||
|
//
|
||||||
|
// @Tags command_template
|
||||||
|
// @Param template body model.CommandTemplate true "command template"
|
||||||
|
// @Success 200 {object} HttpResponse
|
||||||
|
// @Router /command_template [post]
|
||||||
|
func (c *Controller) CreateCommandTemplate(ctx *gin.Context) {
|
||||||
|
template := &model.CommandTemplate{}
|
||||||
|
doCreate(ctx, true, template, config.RESOURCE_AUTHORIZATION, commandTemplatePreHooks...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCommandTemplate godoc
|
||||||
|
//
|
||||||
|
// @Tags command_template
|
||||||
|
// @Param id path int true "template id"
|
||||||
|
// @Success 200 {object} HttpResponse
|
||||||
|
// @Router /command_template/:id [delete]
|
||||||
|
func (c *Controller) DeleteCommandTemplate(ctx *gin.Context) {
|
||||||
|
doDelete(ctx, true, &model.CommandTemplate{}, config.RESOURCE_AUTHORIZATION, commandTemplateDeleteChecks...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCommandTemplate godoc
|
||||||
|
//
|
||||||
|
// @Tags command_template
|
||||||
|
// @Param id path int true "template id"
|
||||||
|
// @Param template body model.CommandTemplate true "command template"
|
||||||
|
// @Success 200 {object} HttpResponse
|
||||||
|
// @Router /command_template/:id [put]
|
||||||
|
func (c *Controller) UpdateCommandTemplate(ctx *gin.Context) {
|
||||||
|
template := &model.CommandTemplate{}
|
||||||
|
doUpdate(ctx, true, template, config.RESOURCE_AUTHORIZATION, commandTemplatePreHooks...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCommandTemplates godoc
|
||||||
|
//
|
||||||
|
// @Tags command_template
|
||||||
|
// @Param page_index query int false "page index"
|
||||||
|
// @Param page_size query int false "page size"
|
||||||
|
// @Param category query string false "template category"
|
||||||
|
// @Param builtin query bool false "filter by builtin status"
|
||||||
|
// @Param info query bool false "info mode"
|
||||||
|
// @Success 200 {object} HttpResponse{data=[]model.CommandTemplate}
|
||||||
|
// @Router /command_template [get]
|
||||||
|
func (c *Controller) GetCommandTemplates(ctx *gin.Context) {
|
||||||
|
info := cast.ToBool(ctx.Query("info"))
|
||||||
|
|
||||||
|
// Build base query using service layer
|
||||||
|
db, err := commandTemplateService.BuildQuery(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusInternalServerError, &pkgErrors.ApiError{Code: pkgErrors.ErrInternal, Data: map[string]any{"err": err}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if category := ctx.Query("category"); category != "" {
|
||||||
|
db = db.Where("category = ?", category)
|
||||||
|
}
|
||||||
|
|
||||||
|
if builtinStr := ctx.Query("builtin"); builtinStr != "" {
|
||||||
|
builtin := cast.ToBool(builtinStr)
|
||||||
|
db = db.Where("is_builtin = ?", builtin)
|
||||||
|
}
|
||||||
|
|
||||||
|
doGet(ctx, !info, db, config.RESOURCE_AUTHORIZATION, commandTemplatePostHooks...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBuiltInCommandTemplates godoc
|
||||||
|
//
|
||||||
|
// @Tags command_template
|
||||||
|
// @Success 200 {object} HttpResponse{data=[]model.CommandTemplate}
|
||||||
|
// @Router /command_template/builtin [get]
|
||||||
|
func (c *Controller) GetBuiltInCommandTemplates(ctx *gin.Context) {
|
||||||
|
templates, err := commandTemplateService.GetBuiltInTemplates(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusInternalServerError, &pkgErrors.ApiError{Code: pkgErrors.ErrInternal, Data: map[string]any{"err": err}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, NewHttpResponseWithData(templates))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTemplateCommands godoc
|
||||||
|
//
|
||||||
|
// @Tags command_template
|
||||||
|
// @Param id path int true "template id"
|
||||||
|
// @Success 200 {object} HttpResponse{data=[]model.Command}
|
||||||
|
// @Router /command_template/:id/commands [get]
|
||||||
|
func (c *Controller) GetTemplateCommands(ctx *gin.Context) {
|
||||||
|
id := cast.ToInt(ctx.Param("id"))
|
||||||
|
if id == 0 {
|
||||||
|
ctx.AbortWithError(http.StatusBadRequest, &pkgErrors.ApiError{Code: pkgErrors.ErrInvalidArgument, Data: map[string]any{"err": "invalid template id"}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
commands, err := commandTemplateService.GetTemplateCommands(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusInternalServerError, &pkgErrors.ApiError{Code: pkgErrors.ErrInternal, Data: map[string]any{"err": err}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, NewHttpResponseWithData(commands))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command template hooks
|
||||||
|
var (
|
||||||
|
commandTemplatePreHooks = []preHook[*model.CommandTemplate]{
|
||||||
|
func(ctx *gin.Context, data *model.CommandTemplate) {
|
||||||
|
if err := commandTemplateService.ValidateCommandTemplate(data); err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusBadRequest, &pkgErrors.ApiError{Code: pkgErrors.ErrInvalidArgument, Data: map[string]any{"err": err.Error()}})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
commandTemplatePostHooks = []postHook[*model.CommandTemplate]{
|
||||||
|
func(ctx *gin.Context, data []*model.CommandTemplate) {
|
||||||
|
// Add any post-processing logic here
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
commandTemplateDeleteChecks = []deleteCheck{
|
||||||
|
func(ctx *gin.Context, id int) {
|
||||||
|
// Check if template is built-in
|
||||||
|
template, err := commandTemplateService.GetCommandTemplate(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(http.StatusInternalServerError, &pkgErrors.ApiError{Code: pkgErrors.ErrInternal, Data: map[string]any{"err": err}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if template != nil && template.IsBuiltin {
|
||||||
|
ctx.AbortWithError(http.StatusBadRequest, &pkgErrors.ApiError{Code: pkgErrors.ErrInvalidArgument, Data: map[string]any{"err": "cannot delete built-in command template"}})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
@@ -7,6 +7,29 @@ import (
|
|||||||
"gorm.io/plugin/soft_delete"
|
"gorm.io/plugin/soft_delete"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CommandCategory defines command categories
|
||||||
|
type CommandCategory string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CategorySecurity CommandCategory = "security" // Security related
|
||||||
|
CategorySystem CommandCategory = "system" // System operations
|
||||||
|
CategoryDatabase CommandCategory = "database" // Database operations
|
||||||
|
CategoryNetwork CommandCategory = "network" // Network operations
|
||||||
|
CategoryFile CommandCategory = "file" // File operations
|
||||||
|
CategoryDeveloper CommandCategory = "developer" // Development related
|
||||||
|
CategoryCustom CommandCategory = "custom" // Custom commands
|
||||||
|
)
|
||||||
|
|
||||||
|
// CommandRiskLevel defines risk levels
|
||||||
|
type CommandRiskLevel int
|
||||||
|
|
||||||
|
const (
|
||||||
|
RiskLevelSafe CommandRiskLevel = 0 // Safe commands
|
||||||
|
RiskLevelWarning CommandRiskLevel = 1 // Warning level
|
||||||
|
RiskLevelDanger CommandRiskLevel = 2 // Dangerous commands
|
||||||
|
RiskLevelCritical CommandRiskLevel = 3 // Critical danger
|
||||||
|
)
|
||||||
|
|
||||||
type Command struct {
|
type Command struct {
|
||||||
Id int `json:"id" gorm:"column:id;primarykey;autoIncrement"`
|
Id int `json:"id" gorm:"column:id;primarykey;autoIncrement"`
|
||||||
Name string `json:"name" gorm:"column:name;uniqueIndex:name_del;size:128"`
|
Name string `json:"name" gorm:"column:name;uniqueIndex:name_del;size:128"`
|
||||||
@@ -15,6 +38,13 @@ type Command struct {
|
|||||||
Enable bool `json:"enable" gorm:"column:enable"`
|
Enable bool `json:"enable" gorm:"column:enable"`
|
||||||
Re *regexp.Regexp `json:"-" gorm:"-"`
|
Re *regexp.Regexp `json:"-" gorm:"-"`
|
||||||
|
|
||||||
|
// Enhanced fields for better management
|
||||||
|
Category CommandCategory `json:"category" gorm:"column:category;default:'custom'"`
|
||||||
|
RiskLevel CommandRiskLevel `json:"risk_level" gorm:"column:risk_level;default:0"`
|
||||||
|
Description string `json:"description" gorm:"column:description"`
|
||||||
|
Tags Slice[string] `json:"tags" gorm:"column:tags;type:json"`
|
||||||
|
IsGlobal bool `json:"is_global" gorm:"column:is_global;default:false"` // Global predefined command
|
||||||
|
|
||||||
Permissions []string `json:"permissions" gorm:"-"`
|
Permissions []string `json:"permissions" gorm:"-"`
|
||||||
ResourceId int `json:"resource_id" gorm:"column:resource_id"`
|
ResourceId int `json:"resource_id" gorm:"column:resource_id"`
|
||||||
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
|
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
|
||||||
@@ -52,3 +82,70 @@ func (m *Command) GetId() int {
|
|||||||
func (m *Command) SetPerms(perms []string) {
|
func (m *Command) SetPerms(perms []string) {
|
||||||
m.Permissions = perms
|
m.Permissions = perms
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetRiskLevelText returns human readable risk level
|
||||||
|
func (m *Command) GetRiskLevelText() string {
|
||||||
|
switch m.RiskLevel {
|
||||||
|
case RiskLevelSafe:
|
||||||
|
return "Safe"
|
||||||
|
case RiskLevelWarning:
|
||||||
|
return "Warning"
|
||||||
|
case RiskLevelDanger:
|
||||||
|
return "Danger"
|
||||||
|
case RiskLevelCritical:
|
||||||
|
return "Critical"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommandTemplate represents predefined command templates
|
||||||
|
type CommandTemplate struct {
|
||||||
|
Id int `json:"id" gorm:"column:id;primarykey;autoIncrement"`
|
||||||
|
Name string `json:"name" gorm:"column:name;size:128"`
|
||||||
|
Description string `json:"description" gorm:"column:description"`
|
||||||
|
Category CommandCategory `json:"category" gorm:"column:category"`
|
||||||
|
CmdIds Slice[int] `json:"cmd_ids" gorm:"column:cmd_ids;type:json"`
|
||||||
|
IsBuiltin bool `json:"is_builtin" gorm:"column:is_builtin;default:false"` // Built-in template
|
||||||
|
ResourceId int `json:"resource_id" gorm:"column:resource_id"`
|
||||||
|
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
|
||||||
|
UpdaterId int `json:"updater_id" gorm:"column:updater_id"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CommandTemplate) TableName() string {
|
||||||
|
return "command_template"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CommandTemplate) GetName() string {
|
||||||
|
return m.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CommandTemplate) GetId() int {
|
||||||
|
return m.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CommandTemplate) SetId(id int) {
|
||||||
|
m.Id = id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CommandTemplate) SetCreatorId(creatorId int) {
|
||||||
|
m.CreatorId = creatorId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CommandTemplate) SetUpdaterId(updaterId int) {
|
||||||
|
m.UpdaterId = updaterId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CommandTemplate) SetResourceId(resourceId int) {
|
||||||
|
m.ResourceId = resourceId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CommandTemplate) GetResourceId() int {
|
||||||
|
return m.ResourceId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *CommandTemplate) SetPerms(perms []string) {
|
||||||
|
// CommandTemplate doesn't have permissions field, but interface requires it
|
||||||
|
}
|
||||||
|
123
backend/internal/repository/command_template.go
Normal file
123
backend/internal/repository/command_template.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/veops/oneterm/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ICommandTemplateRepository defines the interface for command template data access
|
||||||
|
type ICommandTemplateRepository interface {
|
||||||
|
Create(ctx context.Context, template *model.CommandTemplate) error
|
||||||
|
GetByID(ctx context.Context, id int) (*model.CommandTemplate, error)
|
||||||
|
GetByName(ctx context.Context, name string) (*model.CommandTemplate, error)
|
||||||
|
List(ctx context.Context, offset, limit int, category string, builtin *bool) ([]*model.CommandTemplate, int64, error)
|
||||||
|
Update(ctx context.Context, template *model.CommandTemplate) error
|
||||||
|
Delete(ctx context.Context, id int) error
|
||||||
|
GetBuiltInTemplates(ctx context.Context) ([]*model.CommandTemplate, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommandTemplateRepository implements ICommandTemplateRepository
|
||||||
|
type CommandTemplateRepository struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCommandTemplateRepository creates a new command template repository
|
||||||
|
func NewCommandTemplateRepository(db *gorm.DB) ICommandTemplateRepository {
|
||||||
|
return &CommandTemplateRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create creates a new command template
|
||||||
|
func (r *CommandTemplateRepository) Create(ctx context.Context, template *model.CommandTemplate) error {
|
||||||
|
return r.db.WithContext(ctx).Create(template).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByID retrieves a command template by ID
|
||||||
|
func (r *CommandTemplateRepository) GetByID(ctx context.Context, id int) (*model.CommandTemplate, error) {
|
||||||
|
var template model.CommandTemplate
|
||||||
|
err := r.db.WithContext(ctx).Where("id = ?", id).First(&template).Error
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &template, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByName retrieves a command template by name
|
||||||
|
func (r *CommandTemplateRepository) GetByName(ctx context.Context, name string) (*model.CommandTemplate, error) {
|
||||||
|
var template model.CommandTemplate
|
||||||
|
err := r.db.WithContext(ctx).Where("name = ?", name).First(&template).Error
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &template, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List retrieves command templates with pagination and filters
|
||||||
|
func (r *CommandTemplateRepository) List(ctx context.Context, offset, limit int, category string, builtin *bool) ([]*model.CommandTemplate, int64, error) {
|
||||||
|
query := r.db.WithContext(ctx).Model(&model.CommandTemplate{})
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if category != "" {
|
||||||
|
query = query.Where("category = ?", category)
|
||||||
|
}
|
||||||
|
if builtin != nil {
|
||||||
|
query = query.Where("is_builtin = ?", *builtin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total count
|
||||||
|
var total int64
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply pagination and ordering
|
||||||
|
var templates []*model.CommandTemplate
|
||||||
|
err := query.Order("is_builtin DESC, created_at DESC").
|
||||||
|
Offset(offset).
|
||||||
|
Limit(limit).
|
||||||
|
Find(&templates).Error
|
||||||
|
|
||||||
|
return templates, total, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates an existing command template
|
||||||
|
func (r *CommandTemplateRepository) Update(ctx context.Context, template *model.CommandTemplate) error {
|
||||||
|
// Don't allow updating built-in templates
|
||||||
|
if template.IsBuiltin {
|
||||||
|
return errors.New("cannot update built-in command template")
|
||||||
|
}
|
||||||
|
return r.db.WithContext(ctx).Save(template).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete soft deletes a command template
|
||||||
|
func (r *CommandTemplateRepository) Delete(ctx context.Context, id int) error {
|
||||||
|
// Check if it's a built-in template
|
||||||
|
var template model.CommandTemplate
|
||||||
|
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&template).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if template.IsBuiltin {
|
||||||
|
return errors.New("cannot delete built-in command template")
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.db.WithContext(ctx).Delete(&model.CommandTemplate{}, id).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBuiltInTemplates retrieves all built-in command templates
|
||||||
|
func (r *CommandTemplateRepository) GetBuiltInTemplates(ctx context.Context) ([]*model.CommandTemplate, error) {
|
||||||
|
var templates []*model.CommandTemplate
|
||||||
|
err := r.db.WithContext(ctx).Where("is_builtin = ?", true).
|
||||||
|
Order("category, id").
|
||||||
|
Find(&templates).Error
|
||||||
|
return templates, err
|
||||||
|
}
|
@@ -55,6 +55,17 @@ func (s *CommandService) BuildQuery(ctx *gin.Context) (*gorm.DB, error) {
|
|||||||
db = db.Where("id IN ?", lo.Map(strings.Split(q, ","), func(s string, _ int) int { return cast.ToInt(s) }))
|
db = db.Where("id IN ?", lo.Map(strings.Split(q, ","), func(s string, _ int) int { return cast.ToInt(s) }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply category filter
|
||||||
|
if category := ctx.Query("category"); category != "" {
|
||||||
|
db = db.Where("category = ?", category)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply risk level filter
|
||||||
|
if riskLevelStr := ctx.Query("risk_level"); riskLevelStr != "" {
|
||||||
|
riskLevel := cast.ToInt(riskLevelStr)
|
||||||
|
db = db.Where("risk_level = ?", riskLevel)
|
||||||
|
}
|
||||||
|
|
||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
377
backend/internal/service/command_init.go
Normal file
377
backend/internal/service/command_init.go
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
|
||||||
|
myi18n "github.com/veops/oneterm/internal/i18n"
|
||||||
|
"github.com/veops/oneterm/internal/model"
|
||||||
|
"github.com/veops/oneterm/pkg/db"
|
||||||
|
"github.com/veops/oneterm/pkg/logger"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CommandDefinition defines a command with i18n message keys
|
||||||
|
type CommandDefinition struct {
|
||||||
|
NameKey *i18n.Message
|
||||||
|
DescriptionKey *i18n.Message
|
||||||
|
Cmd string
|
||||||
|
IsRe bool
|
||||||
|
RiskLevel model.CommandRiskLevel
|
||||||
|
Category model.CommandCategory
|
||||||
|
TagKeys []*i18n.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateDefinition defines a template with i18n message keys
|
||||||
|
type TemplateDefinition struct {
|
||||||
|
NameKey *i18n.Message
|
||||||
|
DescriptionKey *i18n.Message
|
||||||
|
Category model.CommandCategory
|
||||||
|
CommandRefs []string // References to command names by i18n key IDs
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLocalizer creates a localizer for the given context (defaults to English)
|
||||||
|
func getLocalizer(ctx context.Context) *i18n.Localizer {
|
||||||
|
// Default to English, as this runs during system startup without user context
|
||||||
|
return i18n.NewLocalizer(myi18n.Bundle, "en")
|
||||||
|
}
|
||||||
|
|
||||||
|
// localizeMessage localizes a message using the given localizer
|
||||||
|
func localizeMessage(localizer *i18n.Localizer, msg *i18n.Message) string {
|
||||||
|
result, _ := localizer.Localize(&i18n.LocalizeConfig{DefaultMessage: msg})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// localizeTags localizes an array of tag message keys
|
||||||
|
func localizeTags(localizer *i18n.Localizer, tagKeys []*i18n.Message) []string {
|
||||||
|
return lo.Map(tagKeys, func(msg *i18n.Message, _ int) string {
|
||||||
|
return localizeMessage(localizer, msg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitBuiltinCommands initializes predefined dangerous commands on system startup
|
||||||
|
func InitBuiltinCommands() error {
|
||||||
|
ctx := context.Background()
|
||||||
|
localizer := getLocalizer(ctx)
|
||||||
|
|
||||||
|
logger.L().Info("Starting initialization of predefined dangerous commands")
|
||||||
|
|
||||||
|
// Initialize commands
|
||||||
|
commands := getPreDefinedCommands()
|
||||||
|
for _, cmdDef := range commands {
|
||||||
|
if err := createCommandIfNotExists(localizer, cmdDef); err != nil {
|
||||||
|
logger.L().Error("Failed to create predefined command",
|
||||||
|
zap.String("name_key", cmdDef.NameKey.ID),
|
||||||
|
zap.Error(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize templates
|
||||||
|
templates := getPreDefinedTemplates()
|
||||||
|
for _, tmplDef := range templates {
|
||||||
|
if err := createTemplateIfNotExists(localizer, tmplDef); err != nil {
|
||||||
|
logger.L().Error("Failed to create predefined template",
|
||||||
|
zap.String("name_key", tmplDef.NameKey.ID),
|
||||||
|
zap.Error(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.L().Info("Predefined dangerous commands initialization completed successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createCommandIfNotExists creates a command if it doesn't exist
|
||||||
|
func createCommandIfNotExists(localizer *i18n.Localizer, cmdDef CommandDefinition) error {
|
||||||
|
name := localizeMessage(localizer, cmdDef.NameKey)
|
||||||
|
|
||||||
|
var existingCmd model.Command
|
||||||
|
err := db.DB.Where("name = ? AND is_global = ?", name, true).First(&existingCmd).Error
|
||||||
|
if err == nil {
|
||||||
|
logger.L().Debug("Predefined command already exists, skipping", zap.String("name", name))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != gorm.ErrRecordNotFound {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new command
|
||||||
|
newCmd := &model.Command{
|
||||||
|
Name: name,
|
||||||
|
Cmd: cmdDef.Cmd,
|
||||||
|
IsRe: cmdDef.IsRe,
|
||||||
|
Enable: true,
|
||||||
|
Category: cmdDef.Category,
|
||||||
|
RiskLevel: cmdDef.RiskLevel,
|
||||||
|
Description: localizeMessage(localizer, cmdDef.DescriptionKey),
|
||||||
|
Tags: localizeTags(localizer, cmdDef.TagKeys),
|
||||||
|
IsGlobal: true,
|
||||||
|
CreatorId: 1, // System user
|
||||||
|
UpdaterId: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.DB.Create(newCmd).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.L().Info("Successfully created predefined command",
|
||||||
|
zap.String("name", name),
|
||||||
|
zap.Int("risk_level", int(cmdDef.RiskLevel)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createTemplateIfNotExists creates a template if it doesn't exist
|
||||||
|
func createTemplateIfNotExists(localizer *i18n.Localizer, tmplDef TemplateDefinition) error {
|
||||||
|
name := localizeMessage(localizer, tmplDef.NameKey)
|
||||||
|
|
||||||
|
var existingTemplate model.CommandTemplate
|
||||||
|
err := db.DB.Where("name = ? AND is_builtin = ?", name, true).First(&existingTemplate).Error
|
||||||
|
if err == nil {
|
||||||
|
logger.L().Debug("Predefined template already exists, skipping", zap.String("name", name))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != gorm.ErrRecordNotFound {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get command IDs by localized names
|
||||||
|
cmdIds, err := getCommandIdsByLocalizedNames(localizer, tmplDef.CommandRefs)
|
||||||
|
if err != nil {
|
||||||
|
logger.L().Warn("Some commands not found for template",
|
||||||
|
zap.String("template", name),
|
||||||
|
zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new template
|
||||||
|
newTemplate := &model.CommandTemplate{
|
||||||
|
Name: name,
|
||||||
|
Description: localizeMessage(localizer, tmplDef.DescriptionKey),
|
||||||
|
Category: tmplDef.Category,
|
||||||
|
CmdIds: cmdIds,
|
||||||
|
IsBuiltin: true,
|
||||||
|
CreatorId: 1, // System user
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.DB.Create(newTemplate).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.L().Info("Successfully created predefined template",
|
||||||
|
zap.String("name", name),
|
||||||
|
zap.Int("command_count", len(cmdIds)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCommandIdsByLocalizedNames retrieves command IDs by their localized names
|
||||||
|
func getCommandIdsByLocalizedNames(localizer *i18n.Localizer, nameKeys []string) (model.Slice[int], error) {
|
||||||
|
// Convert i18n keys to localized names
|
||||||
|
localizedNames := lo.Map(nameKeys, func(keyID string, _ int) string {
|
||||||
|
// Find the message by ID in our predefined commands
|
||||||
|
cmdDefs := getPreDefinedCommands()
|
||||||
|
if cmdDef, found := lo.Find(cmdDefs, func(def CommandDefinition) bool {
|
||||||
|
return def.NameKey.ID == keyID
|
||||||
|
}); found {
|
||||||
|
return localizeMessage(localizer, cmdDef.NameKey)
|
||||||
|
}
|
||||||
|
return keyID // fallback to key ID if not found
|
||||||
|
})
|
||||||
|
|
||||||
|
var commands []model.Command
|
||||||
|
if err := db.DB.Where("name IN ? AND is_global = ?", localizedNames, true).Find(&commands).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return lo.Map(commands, func(cmd model.Command, _ int) int {
|
||||||
|
return cmd.Id
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPreDefinedCommands returns command definitions with i18n message keys
|
||||||
|
func getPreDefinedCommands() []CommandDefinition {
|
||||||
|
return []CommandDefinition{
|
||||||
|
// Critical dangerous commands (RiskLevel: 3)
|
||||||
|
{
|
||||||
|
NameKey: myi18n.CmdDeleteRootDir,
|
||||||
|
DescriptionKey: myi18n.CmdDeleteRootDirDesc,
|
||||||
|
Cmd: "^rm\\s+(-rf?|--recursive\\s+--force?)\\s+/\\s*$",
|
||||||
|
IsRe: true,
|
||||||
|
RiskLevel: model.RiskLevelCritical,
|
||||||
|
Category: model.CategorySecurity,
|
||||||
|
TagKeys: []*i18n.Message{myi18n.TagDangerous, myi18n.TagDelete, myi18n.TagSystem},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NameKey: myi18n.CmdDeleteSystemDirs,
|
||||||
|
DescriptionKey: myi18n.CmdDeleteSystemDirsDesc,
|
||||||
|
Cmd: "^rm\\s+(-rf?|--recursive\\s+--force?)\\s+/(bin|boot|dev|etc|lib|lib64|proc|root|sbin|sys|usr)(/.*)?$",
|
||||||
|
IsRe: true,
|
||||||
|
RiskLevel: model.RiskLevelCritical,
|
||||||
|
Category: model.CategorySecurity,
|
||||||
|
TagKeys: []*i18n.Message{myi18n.TagDangerous, myi18n.TagDelete, myi18n.TagSystemDirs},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NameKey: myi18n.CmdDiskDestruction,
|
||||||
|
DescriptionKey: myi18n.CmdDiskDestructionDesc,
|
||||||
|
Cmd: "^dd\\s+if=/dev/(zero|random|urandom)\\s+of=/dev/[sh]d[a-z]+",
|
||||||
|
IsRe: true,
|
||||||
|
RiskLevel: model.RiskLevelCritical,
|
||||||
|
Category: model.CategorySecurity,
|
||||||
|
TagKeys: []*i18n.Message{myi18n.TagDangerous, myi18n.TagDisk, myi18n.TagDestruction},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NameKey: myi18n.CmdFormatDisk,
|
||||||
|
DescriptionKey: myi18n.CmdFormatDiskDesc,
|
||||||
|
Cmd: "^(mkfs|fdisk|parted|gdisk)\\.(ext[234]|xfs|btrfs|ntfs|fat32)\\s+/dev/[sh]d[a-z]+",
|
||||||
|
IsRe: true,
|
||||||
|
RiskLevel: model.RiskLevelCritical,
|
||||||
|
Category: model.CategorySecurity,
|
||||||
|
TagKeys: []*i18n.Message{myi18n.TagDangerous, myi18n.TagFormat, myi18n.TagDisk},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NameKey: myi18n.CmdForkBomb,
|
||||||
|
DescriptionKey: myi18n.CmdForkBombDesc,
|
||||||
|
Cmd: ":(\\)|\\{\\s*\\|\\s*:\\s*&\\s*\\};\\s*:",
|
||||||
|
IsRe: true,
|
||||||
|
RiskLevel: model.RiskLevelCritical,
|
||||||
|
Category: model.CategorySecurity,
|
||||||
|
TagKeys: []*i18n.Message{myi18n.TagDangerous, myi18n.TagAttack, myi18n.TagResourceExhaustion},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Dangerous commands (RiskLevel: 2)
|
||||||
|
{
|
||||||
|
NameKey: myi18n.CmdSystemReboot,
|
||||||
|
DescriptionKey: myi18n.CmdSystemRebootDesc,
|
||||||
|
Cmd: "^(shutdown|reboot|halt|poweroff|init\\s+[06])\\b",
|
||||||
|
IsRe: true,
|
||||||
|
RiskLevel: model.RiskLevelDanger,
|
||||||
|
Category: model.CategorySystem,
|
||||||
|
TagKeys: []*i18n.Message{myi18n.TagReboot, myi18n.TagShutdown, myi18n.TagSystem},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NameKey: myi18n.CmdModifySystemFiles,
|
||||||
|
DescriptionKey: myi18n.CmdModifySystemFilesDesc,
|
||||||
|
Cmd: "^(vi|vim|nano|emacs|cat\\s*>|echo\\s*>)\\s+/(etc|boot|sys)/.+",
|
||||||
|
IsRe: true,
|
||||||
|
RiskLevel: model.RiskLevelDanger,
|
||||||
|
Category: model.CategorySystem,
|
||||||
|
TagKeys: []*i18n.Message{myi18n.TagEdit, myi18n.TagSystemFiles, myi18n.TagConfig},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NameKey: myi18n.CmdDropDatabase,
|
||||||
|
DescriptionKey: myi18n.CmdDropDatabaseDesc,
|
||||||
|
Cmd: "^(mysql|psql|mongo).*drop\\s+(database|schema|collection)\\s+\\w+",
|
||||||
|
IsRe: true,
|
||||||
|
RiskLevel: model.RiskLevelDanger,
|
||||||
|
Category: model.CategoryDatabase,
|
||||||
|
TagKeys: []*i18n.Message{myi18n.TagDatabase, myi18n.TagDelete, myi18n.TagDrop},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NameKey: myi18n.CmdTruncateTable,
|
||||||
|
DescriptionKey: myi18n.CmdTruncateTableDesc,
|
||||||
|
Cmd: "^(mysql|psql).*\\b(truncate\\s+table|delete\\s+from\\s+\\w+\\s*;?)\\s*$",
|
||||||
|
IsRe: true,
|
||||||
|
RiskLevel: model.RiskLevelDanger,
|
||||||
|
Category: model.CategoryDatabase,
|
||||||
|
TagKeys: []*i18n.Message{myi18n.TagDatabase, myi18n.TagClear, myi18n.TagTruncate},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NameKey: myi18n.CmdModifyPermissions,
|
||||||
|
DescriptionKey: myi18n.CmdModifyPermissionsDesc,
|
||||||
|
Cmd: "^(chmod|chown|chgrp)\\s+(777|666|755)\\s+/",
|
||||||
|
IsRe: true,
|
||||||
|
RiskLevel: model.RiskLevelDanger,
|
||||||
|
Category: model.CategorySecurity,
|
||||||
|
TagKeys: []*i18n.Message{myi18n.TagPermissions, myi18n.TagChmod, myi18n.TagSecurity},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Warning level commands (RiskLevel: 1)
|
||||||
|
{
|
||||||
|
NameKey: myi18n.CmdDropTable,
|
||||||
|
DescriptionKey: myi18n.CmdDropTableDesc,
|
||||||
|
Cmd: "^(mysql|psql).*drop\\s+table\\s+\\w+",
|
||||||
|
IsRe: true,
|
||||||
|
RiskLevel: model.RiskLevelWarning,
|
||||||
|
Category: model.CategoryDatabase,
|
||||||
|
TagKeys: []*i18n.Message{myi18n.TagDatabase, myi18n.TagDelete, myi18n.TagTable},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NameKey: myi18n.CmdServiceControl,
|
||||||
|
DescriptionKey: myi18n.CmdServiceControlDesc,
|
||||||
|
Cmd: "^(systemctl|service)\\s+(stop|restart|disable)\\s+\\w+",
|
||||||
|
IsRe: true,
|
||||||
|
RiskLevel: model.RiskLevelWarning,
|
||||||
|
Category: model.CategorySystem,
|
||||||
|
TagKeys: []*i18n.Message{myi18n.TagService, myi18n.TagSystemctl, myi18n.TagControl},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NameKey: myi18n.CmdNetworkConfig,
|
||||||
|
DescriptionKey: myi18n.CmdNetworkConfigDesc,
|
||||||
|
Cmd: "^(iptables|ufw|firewall-cmd|ip\\s+route)\\s+",
|
||||||
|
IsRe: true,
|
||||||
|
RiskLevel: model.RiskLevelWarning,
|
||||||
|
Category: model.CategoryNetwork,
|
||||||
|
TagKeys: []*i18n.Message{myi18n.TagNetwork, myi18n.TagFirewall, myi18n.TagRouting},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NameKey: myi18n.CmdUserManagement,
|
||||||
|
DescriptionKey: myi18n.CmdUserManagementDesc,
|
||||||
|
Cmd: "^(useradd|userdel|usermod|passwd)\\s+",
|
||||||
|
IsRe: true,
|
||||||
|
RiskLevel: model.RiskLevelWarning,
|
||||||
|
Category: model.CategorySecurity,
|
||||||
|
TagKeys: []*i18n.Message{myi18n.TagUser, myi18n.TagManagement, myi18n.TagSecurity},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NameKey: myi18n.CmdKernelModule,
|
||||||
|
DescriptionKey: myi18n.CmdKernelModuleDesc,
|
||||||
|
Cmd: "^(modprobe|rmmod|insmod)\\s+",
|
||||||
|
IsRe: true,
|
||||||
|
RiskLevel: model.RiskLevelWarning,
|
||||||
|
Category: model.CategorySystem,
|
||||||
|
TagKeys: []*i18n.Message{myi18n.TagKernel, myi18n.TagModule, myi18n.TagSystem},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPreDefinedTemplates returns template definitions with i18n message keys
|
||||||
|
func getPreDefinedTemplates() []TemplateDefinition {
|
||||||
|
return []TemplateDefinition{
|
||||||
|
{
|
||||||
|
NameKey: myi18n.TmplBasicSecurity,
|
||||||
|
DescriptionKey: myi18n.TmplBasicSecurityDesc,
|
||||||
|
Category: model.CategorySecurity,
|
||||||
|
CommandRefs: []string{"CmdDeleteRootDir", "CmdDeleteSystemDirs", "CmdDiskDestruction", "CmdFormatDisk", "CmdForkBomb"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NameKey: myi18n.TmplDatabaseProtection,
|
||||||
|
DescriptionKey: myi18n.TmplDatabaseProtectionDesc,
|
||||||
|
Category: model.CategoryDatabase,
|
||||||
|
CommandRefs: []string{"CmdDropDatabase", "CmdTruncateTable", "CmdDropTable"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NameKey: myi18n.TmplServiceRestrictions,
|
||||||
|
DescriptionKey: myi18n.TmplServiceRestrictionsDesc,
|
||||||
|
Category: model.CategorySystem,
|
||||||
|
CommandRefs: []string{"CmdSystemReboot", "CmdModifySystemFiles", "CmdServiceControl"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NameKey: myi18n.TmplNetworkSecurity,
|
||||||
|
DescriptionKey: myi18n.TmplNetworkSecurityDesc,
|
||||||
|
Category: model.CategorySecurity,
|
||||||
|
CommandRefs: []string{"CmdNetworkConfig", "CmdModifyPermissions", "CmdUserManagement"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NameKey: myi18n.TmplDevEnvironment,
|
||||||
|
DescriptionKey: myi18n.TmplDevEnvironmentDesc,
|
||||||
|
Category: model.CategorySecurity,
|
||||||
|
CommandRefs: []string{"CmdDeleteRootDir", "CmdFormatDisk", "CmdForkBomb"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
185
backend/internal/service/command_template.go
Normal file
185
backend/internal/service/command_template.go
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/veops/oneterm/internal/model"
|
||||||
|
"github.com/veops/oneterm/internal/repository"
|
||||||
|
dbpkg "github.com/veops/oneterm/pkg/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CommandTemplateService handles business logic for command templates
|
||||||
|
type CommandTemplateService struct {
|
||||||
|
repo repository.ICommandTemplateRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCommandTemplateService creates a new command template service
|
||||||
|
func NewCommandTemplateService() *CommandTemplateService {
|
||||||
|
repo := repository.NewCommandTemplateRepository(dbpkg.DB)
|
||||||
|
return &CommandTemplateService{
|
||||||
|
repo: repo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildQuery builds the base query for command templates
|
||||||
|
func (s *CommandTemplateService) BuildQuery(ctx context.Context) (*gorm.DB, error) {
|
||||||
|
db := dbpkg.DB.Model(model.DefaultCommandTemplate)
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCommandTemplate creates a new command template
|
||||||
|
func (s *CommandTemplateService) CreateCommandTemplate(ctx context.Context, template *model.CommandTemplate) error {
|
||||||
|
// Validate the template
|
||||||
|
if err := s.ValidateCommandTemplate(template); err != nil {
|
||||||
|
return fmt.Errorf("validation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate names
|
||||||
|
existing, err := s.repo.GetByName(ctx, template.Name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check existing template: %w", err)
|
||||||
|
}
|
||||||
|
if existing != nil {
|
||||||
|
return errors.New("template with this name already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default values
|
||||||
|
template.IsBuiltin = false
|
||||||
|
|
||||||
|
return s.repo.Create(ctx, template)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCommandTemplate retrieves a command template by ID
|
||||||
|
func (s *CommandTemplateService) GetCommandTemplate(ctx context.Context, id int) (*model.CommandTemplate, error) {
|
||||||
|
return s.repo.GetByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCommandTemplateByName retrieves a command template by name
|
||||||
|
func (s *CommandTemplateService) GetCommandTemplateByName(ctx context.Context, name string) (*model.CommandTemplate, error) {
|
||||||
|
return s.repo.GetByName(ctx, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListCommandTemplates retrieves command templates with pagination and filters
|
||||||
|
func (s *CommandTemplateService) ListCommandTemplates(ctx context.Context, offset, limit int, category string, builtin *bool) ([]*model.CommandTemplate, int64, error) {
|
||||||
|
return s.repo.List(ctx, offset, limit, category, builtin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCommandTemplate updates an existing command template
|
||||||
|
func (s *CommandTemplateService) UpdateCommandTemplate(ctx context.Context, template *model.CommandTemplate) error {
|
||||||
|
// Validate the template
|
||||||
|
if err := s.ValidateCommandTemplate(template); err != nil {
|
||||||
|
return fmt.Errorf("validation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if template exists
|
||||||
|
existing, err := s.repo.GetByID(ctx, template.Id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get existing template: %w", err)
|
||||||
|
}
|
||||||
|
if existing == nil {
|
||||||
|
return errors.New("command template not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow changing built-in status
|
||||||
|
template.IsBuiltin = existing.IsBuiltin
|
||||||
|
|
||||||
|
return s.repo.Update(ctx, template)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCommandTemplate deletes a command template
|
||||||
|
func (s *CommandTemplateService) DeleteCommandTemplate(ctx context.Context, id int) error {
|
||||||
|
return s.repo.Delete(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBuiltInTemplates retrieves all built-in command templates
|
||||||
|
func (s *CommandTemplateService) GetBuiltInTemplates(ctx context.Context) ([]*model.CommandTemplate, error) {
|
||||||
|
return s.repo.GetBuiltInTemplates(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateCommandTemplate validates a command template
|
||||||
|
func (s *CommandTemplateService) ValidateCommandTemplate(template *model.CommandTemplate) error {
|
||||||
|
if template.Name == "" {
|
||||||
|
return errors.New("template name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(template.Name) > 128 {
|
||||||
|
return errors.New("template name too long (max 128 characters)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if template.Category == "" {
|
||||||
|
return errors.New("template category is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate category
|
||||||
|
validCategories := []string{"security", "system", "database", "network", "file", "developer", "custom"}
|
||||||
|
categoryValid := false
|
||||||
|
for _, cat := range validCategories {
|
||||||
|
if string(template.Category) == cat {
|
||||||
|
categoryValid = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !categoryValid {
|
||||||
|
return fmt.Errorf("invalid category: %s. Valid categories: %v", template.Category, validCategories)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate command IDs if provided
|
||||||
|
if len(template.CmdIds) > 0 {
|
||||||
|
if err := s.validateCommandIds(template.CmdIds); err != nil {
|
||||||
|
return fmt.Errorf("invalid command IDs: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateCommandIds validates that all command IDs exist
|
||||||
|
func (s *CommandTemplateService) validateCommandIds(cmdIds model.Slice[int]) error {
|
||||||
|
if len(cmdIds) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert model.Slice[int] to []int for SQL query
|
||||||
|
ids := make([]int, len(cmdIds))
|
||||||
|
copy(ids, cmdIds)
|
||||||
|
|
||||||
|
// Check if all command IDs exist
|
||||||
|
var count int64
|
||||||
|
err := dbpkg.DB.Model(&model.Command{}).Where("id IN ?", ids).Count(&count).Error
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to validate command IDs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if int(count) != len(cmdIds) {
|
||||||
|
return errors.New("some command IDs do not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTemplateCommands retrieves all commands for a template
|
||||||
|
func (s *CommandTemplateService) GetTemplateCommands(ctx context.Context, templateId int) ([]*model.Command, error) {
|
||||||
|
template, err := s.repo.GetByID(ctx, templateId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if template == nil {
|
||||||
|
return nil, errors.New("command template not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(template.CmdIds) == 0 {
|
||||||
|
return []*model.Command{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert model.Slice[int] to []int for SQL query
|
||||||
|
ids := make([]int, len(template.CmdIds))
|
||||||
|
copy(ids, template.CmdIds)
|
||||||
|
|
||||||
|
var commands []*model.Command
|
||||||
|
err = dbpkg.DB.WithContext(ctx).Where("id IN ?", ids).Find(&commands).Error
|
||||||
|
return commands, err
|
||||||
|
}
|
Reference in New Issue
Block a user