WIP new WebAdmin: event rules

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2024-02-01 20:32:43 +01:00
parent c85601146d
commit ad80d4e475
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
11 changed files with 1264 additions and 1014 deletions

2
go.mod
View file

@ -54,7 +54,7 @@ require (
github.com/rs/xid v1.5.0
github.com/rs/zerolog v1.31.0
github.com/sftpgo/sdk v0.1.6-0.20240114195211-3f4916cc829c
github.com/shirou/gopsutil/v3 v3.23.12
github.com/shirou/gopsutil/v3 v3.24.1
github.com/spf13/afero v1.11.0
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.18.2

4
go.sum
View file

@ -352,8 +352,8 @@ github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sftpgo/sdk v0.1.6-0.20240114195211-3f4916cc829c h1:07TYPvNbOnmKsBxjNsUr+gsILIUWflw1UYwjn1jognM=
github.com/sftpgo/sdk v0.1.6-0.20240114195211-3f4916cc829c/go.mod h1:AWoY2YYe/P1ymfTlRER/meERQjCcZZTbgVPGcPQgaqc=
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
github.com/shirou/gopsutil/v3 v3.24.1 h1:R3t6ondCEvmARp3wxODhXMTLC/klMa87h2PHUw5m7QI=
github.com/shirou/gopsutil/v3 v3.24.1/go.mod h1:UU7a2MSBQa+kW1uuDq8DeEBS8kmrnQwsv2b5O513rwU=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=

View file

@ -117,19 +117,19 @@ func isEventTriggerValid(trigger int) bool {
func getTriggerTypeAsString(trigger int) string {
switch trigger {
case EventTriggerFsEvent:
return "Filesystem event"
return util.I18nTriggerFsEvent
case EventTriggerProviderEvent:
return "Provider event"
return util.I18nTriggerProviderEvent
case EventTriggerIPBlocked:
return "IP blocked"
return util.I18nTriggerIPBlockedEvent
case EventTriggerCertificate:
return "Certificate renewal"
return util.I18nTriggerCertificateRenewEvent
case EventTriggerOnDemand:
return "On demand"
return util.I18nTriggerOnDemandEvent
case EventTriggerIDPLogin:
return "Identity Provider login"
return util.I18nTriggerIDPLoginEvent
default:
return "Schedule"
return util.I18nTriggerScheduleEvent
}
}
@ -1212,17 +1212,26 @@ func (a *EventAction) getACopy() EventAction {
func (a *EventAction) validateAssociation(trigger int, fsEvents []string) error {
if a.Options.IsFailureAction {
if a.Options.ExecuteSync {
return util.NewValidationError("sync execution is not supported for failure actions")
return util.NewI18nError(
util.NewValidationError("sync execution is not supported for failure actions"),
util.I18nErrorEvSyncFailureActions,
)
}
}
if a.Options.ExecuteSync {
if trigger != EventTriggerFsEvent && trigger != EventTriggerIDPLogin {
return util.NewValidationError("sync execution is only supported for some filesystem events and Identity Provider logins")
return util.NewI18nError(
util.NewValidationError("sync execution is only supported for some filesystem events and Identity Provider logins"),
util.I18nErrorEvSyncUnsupported,
)
}
if trigger == EventTriggerFsEvent {
for _, ev := range fsEvents {
if !util.Contains(allowedSyncFsEvents, ev) {
return util.NewValidationError("sync execution is only supported for upload and pre-* events")
return util.NewI18nError(
util.NewValidationError("sync execution is only supported for upload and pre-* events"),
util.I18nErrorEvSyncUnsupportedFs,
)
}
}
}
@ -1379,11 +1388,14 @@ func (c *EventConditions) getACopy() EventConditions {
func (c *EventConditions) validateSchedules() error {
if len(c.Schedules) == 0 {
return util.NewValidationError("at least one schedule is required")
return util.NewI18nError(
util.NewValidationError("at least one schedule is required"),
util.I18nErrorRuleScheduleRequired,
)
}
for _, schedule := range c.Schedules {
if err := schedule.validate(); err != nil {
return err
return util.NewI18nError(err, util.I18nErrorRuleScheduleInvalid)
}
}
return nil
@ -1397,7 +1409,10 @@ func (c *EventConditions) validate(trigger int) error {
c.Options.ProviderObjects = nil
c.IDPLoginEvent = 0
if len(c.FsEvents) == 0 {
return util.NewValidationError("at least one filesystem event is required")
return util.NewI18nError(
util.NewValidationError("at least one filesystem event is required"),
util.I18nErrorRuleFsEventRequired,
)
}
for _, ev := range c.FsEvents {
if !util.Contains(SupportedFsEvents, ev) {
@ -1414,7 +1429,10 @@ func (c *EventConditions) validate(trigger int) error {
c.Options.MaxFileSize = 0
c.IDPLoginEvent = 0
if len(c.ProviderEvents) == 0 {
return util.NewValidationError("at least one provider event is required")
return util.NewI18nError(
util.NewValidationError("at least one provider event is required"),
util.I18nErrorRuleProviderEventRequired,
)
}
for _, ev := range c.ProviderEvents {
if !util.Contains(SupportedProviderEvents, ev) {
@ -1558,7 +1576,7 @@ func (r *EventRule) isStatusValid() bool {
func (r *EventRule) validate() error {
if r.Name == "" {
return util.NewValidationError("name is mandatory")
return util.NewI18nError(util.NewValidationError("name is mandatory"), util.I18nErrorNameRequired)
}
if !r.isStatusValid() {
return util.NewValidationError(fmt.Sprintf("invalid event rule status: %d", r.Status))
@ -1570,7 +1588,7 @@ func (r *EventRule) validate() error {
return err
}
if len(r.Actions) == 0 {
return util.NewValidationError("at least one action is required")
return util.NewI18nError(util.NewValidationError("at least one action is required"), util.I18nErrorRuleActionRequired)
}
actionNames := make(map[string]bool)
actionOrders := make(map[int]bool)
@ -1581,7 +1599,10 @@ func (r *EventRule) validate() error {
return util.NewValidationError(fmt.Sprintf("invalid action at position %d, name not specified", idx))
}
if actionNames[r.Actions[idx].Name] {
return util.NewValidationError(fmt.Sprintf("duplicated action %q", r.Actions[idx].Name))
return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("duplicated action %q", r.Actions[idx].Name)),
util.I18nErrorRuleDuplicateActions,
)
}
if actionOrders[r.Actions[idx].Order] {
return util.NewValidationError(fmt.Sprintf("duplicated order %d for action %q",
@ -1600,7 +1621,10 @@ func (r *EventRule) validate() error {
actionOrders[r.Actions[idx].Order] = true
}
if len(r.Actions) == failureActions {
return util.NewValidationError("at least a non-failure action is required")
return util.NewI18nError(
util.NewValidationError("at least a non-failure action is required"),
util.I18nErrorRuleFailureActionsOnly,
)
}
if !hasSyncAction {
return r.validateMandatorySyncActions()
@ -1614,7 +1638,13 @@ func (r *EventRule) validateMandatorySyncActions() error {
}
for _, ev := range r.Conditions.FsEvents {
if util.Contains(mandatorySyncFsEvents, ev) {
return util.NewValidationError(fmt.Sprintf("event %q requires at least a sync action", ev))
return util.NewI18nError(
util.NewValidationError(fmt.Sprintf("event %q requires at least a sync action", ev)),
util.I18nErrorRuleSyncActionRequired,
util.I18nErrorArgs(map[string]any{
"val": ev,
}),
)
}
}
return nil

View file

@ -1775,6 +1775,8 @@ func (s *httpdServer) setupWebAdminRoutes() {
s.handleWebUpdateEventActionPost)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), verifyCSRFHeader).
Delete(webAdminEventActionPath+"/{name}", deleteEventAction)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), compressor.Handler, s.refreshCookie).
Get(webAdminEventRulesPath+jsonAPISuffix, getAllRules)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie).
Get(webAdminEventRulesPath, s.handleWebGetEventRules)
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie).

View file

@ -98,7 +98,6 @@ const (
templateMaintenance = "maintenance.html"
templateMFA = "mfa.html"
templateSetup = "adminsetup.html"
pageEventRulesTitle = "Event rules"
defaultQueryLimit = 1000
inversePatternType = "inverse"
)
@ -153,11 +152,6 @@ type basePage struct {
Branding UIBranding
}
type eventRulesPage struct {
basePage
Rules []dataprovider.EventRule
}
type statusPage struct {
basePage
Status *ServicesStatus
@ -312,7 +306,7 @@ type eventRulePage struct {
Protocols []string
ProviderEvents []string
ProviderObjects []string
Error string
Error *util.I18nError
Mode genericPageMode
IsShared bool
}
@ -1105,19 +1099,19 @@ func (s *httpdServer) renderEventActionPage(w http.ResponseWriter, r *http.Reque
}
func (s *httpdServer) renderEventRulePage(w http.ResponseWriter, r *http.Request, rule dataprovider.EventRule,
mode genericPageMode, error string,
mode genericPageMode, err error,
) {
actions, err := s.getWebEventActions(w, r, defaultQueryLimit, true)
if err != nil {
actions, errActions := s.getWebEventActions(w, r, defaultQueryLimit, true)
if errActions != nil {
return
}
var title, currentURL string
switch mode {
case genericPageModeAdd:
title = "Add new event rules"
title = util.I18nAddRuleTitle
currentURL = webAdminEventRulePath
case genericPageModeUpdate:
title = "Update event rules"
title = util.I18nUpdateRuleTitle
currentURL = fmt.Sprintf("%v/%v", webAdminEventRulePath, url.PathEscape(rule.Name))
}
@ -1130,7 +1124,7 @@ func (s *httpdServer) renderEventRulePage(w http.ResponseWriter, r *http.Request
Protocols: dataprovider.SupportedRuleConditionProtocols,
ProviderEvents: dataprovider.SupportedProviderEvents,
ProviderObjects: dataprovider.SupporteRuleConditionProviderObjects,
Error: error,
Error: getI18nError(err),
Mode: mode,
IsShared: s.isShared > 0,
}
@ -2420,74 +2414,66 @@ func getIDPLoginEventFromPostField(r *http.Request) int {
func getEventRuleConditionsFromPostFields(r *http.Request) (dataprovider.EventConditions, error) {
var schedules []dataprovider.Schedule
var names, groupNames, roleNames, fsPaths []dataprovider.ConditionPattern
for k := range r.Form {
if strings.HasPrefix(k, "schedule_hour") {
hour := strings.TrimSpace(r.Form.Get(k))
if hour != "" {
idx := strings.TrimPrefix(k, "schedule_hour")
dayOfWeek := strings.TrimSpace(r.Form.Get(fmt.Sprintf("schedule_day_of_week%s", idx)))
dayOfMonth := strings.TrimSpace(r.Form.Get(fmt.Sprintf("schedule_day_of_month%s", idx)))
month := strings.TrimSpace(r.Form.Get(fmt.Sprintf("schedule_month%s", idx)))
schedules = append(schedules, dataprovider.Schedule{
Hours: hour,
DayOfWeek: dayOfWeek,
DayOfMonth: dayOfMonth,
Month: month,
})
}
}
if strings.HasPrefix(k, "name_pattern") {
pattern := strings.TrimSpace(r.Form.Get(k))
if pattern != "" {
idx := strings.TrimPrefix(k, "name_pattern")
patternType := r.Form.Get(fmt.Sprintf("type_name_pattern%s", idx))
names = append(names, dataprovider.ConditionPattern{
Pattern: pattern,
InverseMatch: patternType == inversePatternType,
})
}
}
if strings.HasPrefix(k, "group_name_pattern") {
pattern := strings.TrimSpace(r.Form.Get(k))
if pattern != "" {
idx := strings.TrimPrefix(k, "group_name_pattern")
patternType := r.Form.Get(fmt.Sprintf("type_group_name_pattern%s", idx))
groupNames = append(groupNames, dataprovider.ConditionPattern{
Pattern: pattern,
InverseMatch: patternType == inversePatternType,
})
}
}
if strings.HasPrefix(k, "role_name_pattern") {
pattern := strings.TrimSpace(r.Form.Get(k))
if pattern != "" {
idx := strings.TrimPrefix(k, "role_name_pattern")
patternType := r.Form.Get(fmt.Sprintf("type_role_name_pattern%s", idx))
roleNames = append(roleNames, dataprovider.ConditionPattern{
Pattern: pattern,
InverseMatch: patternType == inversePatternType,
})
}
}
if strings.HasPrefix(k, "fs_path_pattern") {
pattern := strings.TrimSpace(r.Form.Get(k))
if pattern != "" {
idx := strings.TrimPrefix(k, "fs_path_pattern")
patternType := r.Form.Get(fmt.Sprintf("type_fs_path_pattern%s", idx))
fsPaths = append(fsPaths, dataprovider.ConditionPattern{
Pattern: pattern,
InverseMatch: patternType == inversePatternType,
})
}
scheduleHours := r.Form["schedule_hour"]
scheduleDayOfWeeks := r.Form["schedule_day_of_week"]
scheduleDayOfMonths := r.Form["schedule_day_of_month"]
scheduleMonths := r.Form["schedule_month"]
for idx, hour := range scheduleHours {
if hour != "" {
schedules = append(schedules, dataprovider.Schedule{
Hours: hour,
DayOfWeek: scheduleDayOfWeeks[idx],
DayOfMonth: scheduleDayOfMonths[idx],
Month: scheduleMonths[idx],
})
}
}
for idx, name := range r.Form["name_pattern"] {
if name != "" {
names = append(names, dataprovider.ConditionPattern{
Pattern: name,
InverseMatch: r.Form["type_name_pattern"][idx] == inversePatternType,
})
}
}
for idx, name := range r.Form["group_name_pattern"] {
if name != "" {
groupNames = append(groupNames, dataprovider.ConditionPattern{
Pattern: name,
InverseMatch: r.Form["type_group_name_pattern"][idx] == inversePatternType,
})
}
}
for idx, name := range r.Form["role_name_pattern"] {
if name != "" {
roleNames = append(roleNames, dataprovider.ConditionPattern{
Pattern: name,
InverseMatch: r.Form["type_role_name_pattern"][idx] == inversePatternType,
})
}
}
for idx, name := range r.Form["fs_path_pattern"] {
if name != "" {
fsPaths = append(fsPaths, dataprovider.ConditionPattern{
Pattern: name,
InverseMatch: r.Form["type_fs_path_pattern"][idx] == inversePatternType,
})
}
}
minFileSize, err := util.ParseBytes(r.Form.Get("fs_min_size"))
if err != nil {
return dataprovider.EventConditions{}, fmt.Errorf("invalid min file size: %w", err)
return dataprovider.EventConditions{}, util.NewI18nError(fmt.Errorf("invalid min file size: %w", err), util.I18nErrorInvalidMinSize)
}
maxFileSize, err := util.ParseBytes(r.Form.Get("fs_max_size"))
if err != nil {
return dataprovider.EventConditions{}, fmt.Errorf("invalid max file size: %w", err)
return dataprovider.EventConditions{}, util.NewI18nError(fmt.Errorf("invalid max file size: %w", err), util.I18nErrorInvalidMaxSize)
}
conditions := dataprovider.EventConditions{
FsEvents: r.Form["fs_events"],
@ -2511,38 +2497,86 @@ func getEventRuleConditionsFromPostFields(r *http.Request) (dataprovider.EventCo
func getEventRuleActionsFromPostFields(r *http.Request) ([]dataprovider.EventAction, error) {
var actions []dataprovider.EventAction
for k := range r.Form {
if strings.HasPrefix(k, "action_name") {
name := strings.TrimSpace(r.Form.Get(k))
if name != "" {
idx := strings.TrimPrefix(k, "action_name")
order, err := strconv.Atoi(r.Form.Get(fmt.Sprintf("action_order%s", idx)))
if err != nil {
return actions, fmt.Errorf("invalid order: %w", err)
}
options := r.Form[fmt.Sprintf("action_options%s", idx)]
actions = append(actions, dataprovider.EventAction{
BaseEventAction: dataprovider.BaseEventAction{
Name: name,
},
Order: order + 1,
Options: dataprovider.EventActionOptions{
IsFailureAction: util.Contains(options, "1"),
StopOnFailure: util.Contains(options, "2"),
ExecuteSync: util.Contains(options, "3"),
},
})
names := r.Form["action_name"]
orders := r.Form["action_order"]
for idx, name := range names {
if name != "" {
order, err := strconv.Atoi(orders[idx])
if err != nil {
return actions, fmt.Errorf("invalid order: %w", err)
}
options := r.Form["action_options"+strconv.Itoa(idx)]
actions = append(actions, dataprovider.EventAction{
BaseEventAction: dataprovider.BaseEventAction{
Name: name,
},
Order: order + 1,
Options: dataprovider.EventActionOptions{
IsFailureAction: util.Contains(options, "1"),
StopOnFailure: util.Contains(options, "2"),
ExecuteSync: util.Contains(options, "3"),
},
})
}
}
return actions, nil
}
func updateRepeaterFormRuleFields(r *http.Request) {
for k := range r.Form {
if hasPrefixAndSuffix(k, "schedules[", "][schedule_hour]") {
base, _ := strings.CutSuffix(k, "[schedule_hour]")
r.Form.Add("schedule_hour", strings.TrimSpace(r.Form.Get(k)))
r.Form.Add("schedule_day_of_week", strings.TrimSpace(r.Form.Get(base+"[schedule_day_of_week]")))
r.Form.Add("schedule_day_of_month", strings.TrimSpace(r.Form.Get(base+"[schedule_day_of_month]")))
r.Form.Add("schedule_month", strings.TrimSpace(r.Form.Get(base+"[schedule_month]")))
continue
}
if hasPrefixAndSuffix(k, "name_filters[", "][name_pattern]") {
base, _ := strings.CutSuffix(k, "[name_pattern]")
r.Form.Add("name_pattern", strings.TrimSpace(r.Form.Get(k)))
r.Form.Add("type_name_pattern", strings.TrimSpace(r.Form.Get(base+"[type_name_pattern]")))
continue
}
if hasPrefixAndSuffix(k, "group_name_filters[", "][group_name_pattern]") {
base, _ := strings.CutSuffix(k, "[group_name_pattern]")
r.Form.Add("group_name_pattern", strings.TrimSpace(r.Form.Get(k)))
r.Form.Add("type_group_name_pattern", strings.TrimSpace(r.Form.Get(base+"[type_group_name_pattern]")))
continue
}
if hasPrefixAndSuffix(k, "role_name_filters[", "][role_name_pattern]") {
base, _ := strings.CutSuffix(k, "[role_name_pattern]")
r.Form.Add("role_name_pattern", strings.TrimSpace(r.Form.Get(k)))
r.Form.Add("type_role_name_pattern", strings.TrimSpace(r.Form.Get(base+"[type_role_name_pattern]")))
continue
}
if hasPrefixAndSuffix(k, "path_filters[", "][fs_path_pattern]") {
base, _ := strings.CutSuffix(k, "[fs_path_pattern]")
r.Form.Add("fs_path_pattern", strings.TrimSpace(r.Form.Get(k)))
r.Form.Add("type_fs_path_pattern", strings.TrimSpace(r.Form.Get(base+"[type_fs_path_pattern]")))
continue
}
if hasPrefixAndSuffix(k, "actions[", "][action_name]") {
base, _ := strings.CutSuffix(k, "[action_name]")
order, _ := strings.CutPrefix(k, "actions[")
order, _ = strings.CutSuffix(order, "][action_name]")
r.Form.Add("action_name", strings.TrimSpace(r.Form.Get(k)))
r.Form["action_options"+strconv.Itoa(len(r.Form["action_name"])-1)] = r.Form[base+"[action_options][]"]
r.Form.Add("action_order", order)
continue
}
}
}
func getEventRuleFromPostFields(r *http.Request) (dataprovider.EventRule, error) {
err := r.ParseForm()
if err != nil {
return dataprovider.EventRule{}, util.NewI18nError(err, util.I18nErrorInvalidForm)
}
updateRepeaterFormRuleFields(r)
status, err := strconv.Atoi(r.Form.Get("status"))
if err != nil {
return dataprovider.EventRule{}, fmt.Errorf("invalid status: %w", err)
@ -3789,31 +3823,27 @@ func (s *httpdServer) handleWebUpdateEventActionPost(w http.ResponseWriter, r *h
http.Redirect(w, r, webAdminEventActionsPath, http.StatusSeeOther)
}
func (s *httpdServer) handleWebGetEventRules(w http.ResponseWriter, r *http.Request) {
func getAllRules(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
limit := defaultQueryLimit
if _, ok := r.URL.Query()["qlimit"]; ok {
if lim, err := strconv.Atoi(r.URL.Query().Get("qlimit")); err == nil {
limit = lim
}
}
rules := make([]dataprovider.EventRule, 0, limit)
rules := make([]dataprovider.EventRule, 0, 10)
for {
res, err := dataprovider.GetEventRules(limit, len(rules), dataprovider.OrderASC)
res, err := dataprovider.GetEventRules(defaultQueryLimit, len(rules), dataprovider.OrderASC)
if err != nil {
s.renderInternalServerErrorPage(w, r, err)
sendAPIResponse(w, r, err, getI18NErrorString(err, util.I18nError500Message), http.StatusInternalServerError)
return
}
rules = append(rules, res...)
if len(res) < limit {
if len(res) < defaultQueryLimit {
break
}
}
render.JSON(w, r, rules)
}
data := eventRulesPage{
basePage: s.getBasePageData(pageEventRulesTitle, webAdminEventRulesPath, r),
Rules: rules,
}
func (s *httpdServer) handleWebGetEventRules(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
data := s.getBasePageData(util.I18nRulesTitle, webAdminEventRulesPath, r)
renderAdminTemplate(w, templateEventRules, data)
}
@ -3823,7 +3853,7 @@ func (s *httpdServer) handleWebAddEventRuleGet(w http.ResponseWriter, r *http.Re
Status: 1,
Trigger: dataprovider.EventTriggerFsEvent,
}
s.renderEventRulePage(w, r, rule, genericPageModeAdd, "")
s.renderEventRulePage(w, r, rule, genericPageModeAdd, nil)
}
func (s *httpdServer) handleWebAddEventRulePost(w http.ResponseWriter, r *http.Request) {
@ -3835,7 +3865,7 @@ func (s *httpdServer) handleWebAddEventRulePost(w http.ResponseWriter, r *http.R
}
rule, err := getEventRuleFromPostFields(r)
if err != nil {
s.renderEventRulePage(w, r, rule, genericPageModeAdd, err.Error())
s.renderEventRulePage(w, r, rule, genericPageModeAdd, err)
return
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
@ -3845,7 +3875,7 @@ func (s *httpdServer) handleWebAddEventRulePost(w http.ResponseWriter, r *http.R
return
}
if err = dataprovider.AddEventRule(&rule, claims.Username, ipAddr, claims.Role); err != nil {
s.renderEventRulePage(w, r, rule, genericPageModeAdd, err.Error())
s.renderEventRulePage(w, r, rule, genericPageModeAdd, err)
return
}
http.Redirect(w, r, webAdminEventRulesPath, http.StatusSeeOther)
@ -3856,7 +3886,7 @@ func (s *httpdServer) handleWebUpdateEventRuleGet(w http.ResponseWriter, r *http
name := getURLParam(r, "name")
rule, err := dataprovider.EventRuleExists(name)
if err == nil {
s.renderEventRulePage(w, r, rule, genericPageModeUpdate, "")
s.renderEventRulePage(w, r, rule, genericPageModeUpdate, nil)
} else if errors.Is(err, util.ErrNotFound) {
s.renderNotFoundPage(w, r, err)
} else {
@ -3882,7 +3912,7 @@ func (s *httpdServer) handleWebUpdateEventRulePost(w http.ResponseWriter, r *htt
}
updatedRule, err := getEventRuleFromPostFields(r)
if err != nil {
s.renderEventRulePage(w, r, updatedRule, genericPageModeUpdate, err.Error())
s.renderEventRulePage(w, r, updatedRule, genericPageModeUpdate, err)
return
}
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
@ -3894,7 +3924,7 @@ func (s *httpdServer) handleWebUpdateEventRulePost(w http.ResponseWriter, r *htt
updatedRule.Name = rule.Name
err = dataprovider.UpdateEventRule(&updatedRule, claims.Username, ipAddr, claims.Role)
if err != nil {
s.renderEventRulePage(w, r, updatedRule, genericPageModeUpdate, err.Error())
s.renderEventRulePage(w, r, updatedRule, genericPageModeUpdate, err)
return
}
http.Redirect(w, r, webAdminEventRulesPath, http.StatusSeeOther)

View file

@ -69,8 +69,11 @@ const (
I18nDefenderTitle = "title.defender"
I18nEventsTitle = "title.logs"
I18nActionsTitle = "title.event_actions"
I18nRulesTitle = "title.event_rules"
I18nAddActionTitle = "title.add_action"
I18nUpdateActionTitle = "title.update_action"
I18nAddRuleTitle = "title.add_rule"
I18nUpdateRuleTitle = "title.update_rule"
I18nStatusTitle = "status.desc"
I18nErrorSetupInstallCode = "setup.install_code_mismatch"
I18nInvalidAuth = "general.invalid_auth_request"
@ -278,6 +281,26 @@ const (
I18nActionFsTypeCompress = "actions.fs_types.compress"
I18nActionFsTypeCopy = "actions.fs_types.copy"
I18nActionFsTypeCreateDirs = "actions.fs_types.create_dirs"
I18nTriggerFsEvent = "rules.triggers.fs_event"
I18nTriggerProviderEvent = "rules.triggers.provider_event"
I18nTriggerIPBlockedEvent = "rules.triggers.ip_blocked"
I18nTriggerCertificateRenewEvent = "rules.triggers.certificate_renewal"
I18nTriggerOnDemandEvent = "rules.triggers.on_demand"
I18nTriggerIDPLoginEvent = "rules.triggers.idp_login"
I18nTriggerScheduleEvent = "rules.triggers.schedule"
I18nErrorInvalidMinSize = "rules.invalid_fs_min_size"
I18nErrorInvalidMaxSize = "rules.invalid_fs_max_size"
I18nErrorRuleActionRequired = "rules.action_required"
I18nErrorRuleFsEventRequired = "rules.fs_event_required"
I18nErrorRuleProviderEventRequired = "rules.provider_event_required"
I18nErrorRuleScheduleRequired = "rules.schedule_required"
I18nErrorRuleScheduleInvalid = "rules.schedule_invalid"
I18nErrorRuleDuplicateActions = "rules.duplicate_actions"
I18nErrorEvSyncFailureActions = "rules.sync_failure_actions"
I18nErrorEvSyncUnsupported = "rules.sync_unsupported"
I18nErrorEvSyncUnsupportedFs = "rules.sync_unsupported_fs_event"
I18nErrorRuleFailureActionsOnly = "rules.only_failure_actions"
I18nErrorRuleSyncActionRequired = "rules.sync_action_required"
)
// NewI18nError returns a I18nError wrappring the provided error

View file

@ -63,7 +63,9 @@
"add_ip_list": "Add IP list entry",
"update_ip_list": "Update IP list entry",
"add_action": "Add action",
"update_action": "Update action"
"update_action": "Update action",
"add_rule": "Add rule",
"update_rule": "Update rule"
},
"setup": {
"desc": "To start using SFTPGo you need to create an administrator user",
@ -252,7 +254,12 @@
"timeout": "Timeout",
"env_vars": "Environment variables",
"hours": "Hours",
"paths": "Paths"
"paths": "Paths",
"hour": "Hour",
"day_of_week": "Day of week",
"day_of_month": "Day of month",
"month": "Month",
"options": "Options"
},
"fs": {
"view_file": "View file \"{{- path}}\"",
@ -1003,5 +1010,63 @@
"metadata_string": "Cloud storage metadata for the downloaded file as JSON escaped string",
"uid": "Unique ID"
}
},
"rules": {
"view_manage": "View and manage rules for events",
"trigger": "Trigger",
"run_confirm": "Do you want to execute the selected rule?",
"run_confirm_btn": "Yes, run",
"run_error_generic": "Unable to run the selected rule",
"run_ok": "Rule actions started",
"run": "Run",
"invalid_fs_min_size": "Invalid min size",
"invalid_fs_max_size": "Invalid max size",
"action_required": "At least one action is required",
"fs_event_required": "At least one filesystem event is required",
"provider_event_required": "At least one provider event is required",
"schedule_required": "At least one schedule is required",
"schedule_invalid": "Invalid schedule",
"duplicate_actions": "Duplicate actions detected",
"sync_failure_actions": "Synchronous execution is not supported for failure actions",
"sync_unsupported": "Synchronous execution is only supported for some filesystem events and Identity Provider logins",
"sync_unsupported_fs_event": "Synchronous execution is only supported for upload and pre-* filesystem events",
"only_failure_actions": "At least a non-failure action is required",
"sync_action_required": "Event \"{{val}}\" requires at least a synchronous action",
"scheduler_help": "The scheduler uses UTC time. Hours: 0-23. Day of week: 0-6 (Sun-Sat). Day of month: 1-31. Month: 1-12. Asterisk (*) indicates a match for all the values of the field. e.g. every day of week, every day of month and so on",
"concurrent_run": "Allow concurrent execution from multiple instances",
"protocol_filters": "Protocol filters",
"object_filters": "Object filters",
"name_filters": "Name filters",
"name_filters_help": "Shell-like pattern filters for usernames, folder names. For example \"user*\"\" will match names starting with \"user\". For provider events, this filter is applied to the username of the admin executing the event",
"inverse_match": "Inverse match",
"group_name_filters": "Group name filters",
"group_name_filters_help": "Shell-like pattern filters for group names. For example \"group*\"\" will match group names starting with \"group\"",
"role_name_filters": "Role name filters",
"role_name_filters_help": "Shell-like pattern filters for role names. For example \"role*\"\" will match role names starting with \"role\"",
"path_filters": "Path filters",
"path_filters_help": "Shell-like pattern filters on filesystem event paths. For example \"/adir/*.txt\"\" will match paths in the \"/adir\" directory ending with \".txt\". Double asterisk is supported, for example \"/**/*.txt\" will match any file ending with \".txt\". \"/mydir/**\" will match any entry in \"/mydir\"",
"file_size_limits": "File size limits",
"file_size_limits_help": "0 means no limit. You can use MB/GB suffix",
"min_size": "Minimum size",
"max_size": "Maximum size",
"actions_help": "One or more actions to execute. The \"Execute sync\" option is supported for \"upload\" events and required for \"pre-*\" events and Identity provider login events if the action checks the account",
"option_failure_action": "Failure action",
"option_stop_on_failure": "Stop on failure",
"option_execute_sync": "Synchronous execution",
"no_filter": "No filter means always triggering events",
"action_placeholder": "Select an action",
"triggers": {
"fs_event": "Filesystem events",
"provider_event": "Provider events",
"ip_blocked": "IP blocked",
"certificate_renewal": "Certificate renewal",
"on_demand": "On demand",
"idp_login": "Identity Provider logins",
"schedule": "Schedules"
},
"idp_logins": {
"user": "User login",
"admin": "Admin login"
}
}
}

View file

@ -63,7 +63,9 @@
"add_ip_list": "Aggiungi elemento a lista IP",
"update_ip_list": "Aggiorna elemento lista IP",
"add_action": "Aggiungi azione",
"update_action": "Aggiorna azione"
"update_action": "Aggiorna azione",
"add_rule": "Aggiungi regola",
"update_rule": "Aggiorna regola"
},
"setup": {
"desc": "Per iniziare a utilizzare SFTPGo devi creare un utente amministratore",
@ -252,7 +254,12 @@
"timeout": "Timeout",
"env_vars": "Variabili d'ambiente",
"hours": "Ore",
"paths": "Percorsi"
"paths": "Percorsi",
"hour": "Ora",
"day_of_week": "Giorno settimana",
"day_of_month": "Giorno mese",
"month": "Mese",
"options": "Opzioni"
},
"fs": {
"view_file": "Visualizza file \"{{- path}}\"",
@ -1003,5 +1010,63 @@
"metadata_string": "Metadati del Cloud Storage Provider serializzati come stringa JSON escaped per i file scaricati",
"uid": "ID univoco"
}
},
"rules": {
"view_manage": "Visualizza e gestisci le regole per gli eventi",
"trigger": "Attivazione",
"run_confirm": "Vuoi eseguire la regola selezionata?",
"run_confirm_btn": "Si, esegui",
"run_error_generic": "Impossibile eseguire la regola selezionata",
"run_ok": "Azioni delle regola avviate",
"run": "Esegui",
"invalid_fs_min_size": "Dimensione minima non valida",
"invalid_fs_max_size": "Dimensione massima non valida",
"action_required": "Almeno un'azione è obbligatoria",
"fs_event_required": "Almeno un evento file system è obbligatorio",
"provider_event_required": "Almeno un evento provider è obbligatorio",
"schedule_required": "Almeno una schedulazione è obbligatoria",
"schedule_invalid": "Schedulazione non valida",
"duplicate_actions": "Rilevata azioni duplicate",
"sync_failure_actions": "L'esecuzione sincrona non è supportata per le azioni su errore",
"sync_unsupported": "L'esecuzione sincrona è supportata solo per alcuni eventi del file system e per gli accessi tramite Identity Provider",
"sync_unsupported_fs_event": "L'esecuzione sincrona è supporta solo per gli eventi \"upload\" e \"pre-*\"",
"only_failure_actions": "E' richiesta almeno un'azione che non venga eseguita su errore",
"sync_action_required": "L'evento \"{{val}}\" richiede almeno un'azione da eseguire sincronamente",
"scheduler_help": "Lo scheduler utilizza l'ora UTC. Orari: 0-23. Giorno della settimana: 0-6 (dom-sab). Giorno del mese: 1-31. Mese: 1-12. L'asterisco (*) indica una corrispondenza per tutti i valori del campo. per esempio. ogni giorno della settimana, ogni giorno del mese e così via",
"concurrent_run": "Consentire l'esecuzione simultanea da più istanze",
"protocol_filters": "Filtro su protocolli",
"object_filters": "Filtro su oggetti",
"name_filters": "Filtro su nomi",
"name_filters_help": "Filtri per nomi utente e nomi di cartelle. Ad esempio, \"user*\"\" corrisponderà per i nomi che iniziano con \"user\". Per gli eventi del provider, questo filtro viene applicato al nome utente dell'amministratore che esegue l'evento",
"inverse_match": "Corrispondenza inversa",
"group_name_filters": "Filtro su nome gruppi",
"group_name_filters_help": "Filtri per nomi dei gruppi. Ad esempio \"group*\"\" corrisponderà ai nomi dei gruppi che iniziano con \"group\"",
"role_name_filters": "Filtri su nome ruoli",
"role_name_filters_help": "Filtri per nomi dei ruoli. Ad esempio \"role*\"\" corrisponderà ai nomi dei gruppi che iniziano con \"role\"",
"path_filters": "Filtri sui percorsi",
"path_filters_help": "Filtri sui percorsi degli eventi del file system. Ad esempio \"/adir/*.txt\"\" corrisponderà ai percorsi nella directory \"/adir\" che terminano con \".txt\". È supportato il doppio asterisco, ad esempio \"/**/*. txt\" corrisponderà a qualsiasi file che termina con \".txt\". \"/mydir/**\" corrisponderà a qualsiasi voce in \"/mydir\"",
"file_size_limits": "Filtri sulla dimensione file",
"file_size_limits_help": "0 significa nessun limite. È possibile utilizzare il suffisso MB/GB",
"min_size": "Dimensione min",
"max_size": "Dimensione max",
"actions_help": "Una o più azioni da eseguire. L'opzione \"Esecuzione sincrona\" è supportata per gli eventi di \"upload\" ed è richiesta per gli eventi \"pre-*\" e gli eventi di accesso tramite Identity provider se l'azione controlla l'account",
"option_failure_action": "Azione su errore",
"option_stop_on_failure": "Termina su errore",
"option_execute_sync": "Esecuzione sincrona",
"no_filter": "Nessun filtro significa attivare sempre gli eventi",
"action_placeholder": "Seleziona un'azione",
"triggers": {
"fs_event": "Eventi file system",
"provider_event": "Eventi provider",
"ip_blocked": "IP bloccato",
"certificate_renewal": "Rinnovo certificato",
"on_demand": "Su richiesta",
"idp_login": "Accessi tramite Identity Provider",
"schedule": "Schedulazioni"
},
"idp_logins": {
"user": "Accesso utente",
"admin": "Accesso amministratore"
}
}
}

View file

@ -1020,13 +1020,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
$('#idType').on("change", function(){
onTypeChanged(this.value);
})
});
$('#idFsActionType').on("change", function(){
onFsActionChanged(this.value);
});
$('#role_form').submit(function (event) {
$('#eventaction_form').submit(function (event) {
let submitButton = document.querySelector('#form_submit');
submitButton.setAttribute('data-kt-indicator', 'on');
submitButton.disabled = true;

File diff suppressed because it is too large Load diff

View file

@ -1,326 +1,406 @@
<!--
Copyright (C) 2019 Nicola Murino
Copyright (C) 2024 Nicola Murino
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, version 3.
This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
https://keenthemes.com/products/templates-mega-bundle
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
KeenThemes HTML/CSS/JS components are allowed for use only within the
SFTPGo product and restricted to be used in a resealable HTML template
that can compete with KeenThemes products anyhow.
This WebUI is allowed for use only within the SFTPGo product and
therefore cannot be used in derivative works/products without an
explicit grant from the SFTPGo Team (support@sftpgo.com).
-->
{{template "base" .}}
{{define "title"}}{{.Title}}{{end}}
{{- define "extra_css"}}
<link href="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css" rel="stylesheet" type="text/css"/>
{{- end}}
{{define "extra_css"}}
<link href="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet">
{{end}}
{{define "page_body"}}
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
<span id="errorTxt"></span>
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
<span aria-hidden="true">&times;</span>
</button>
</div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div id="successMsg" class="card mb-4 border-left-success" style="display: none;">
<div id="successTxt" class="card-body"></div>
</div>
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">View and manage event rules</h6>
{{- define "page_body"}}
{{- template "errmsg" ""}}
<div class="card shadow-sm">
<div class="card-header bg-light">
<h3 data-i18n="rules.view_manage" class="card-title section-title">View and manage event rules</h3>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0">
<div id="card_body" class="card-body">
<div id="loader" class="align-items-center text-center my-10">
<span class="spinner-border w-15px h-15px text-muted align-middle me-2"></span>
<span data-i18n="general.loading" class="text-gray-700">Loading...</span>
</div>
<div id="card_content" class="d-none">
<div class="d-flex flex-stack flex-wrap mb-5">
<div class="d-flex align-items-center position-relative my-2">
<i class="ki-solid ki-magnifier fs-1 position-absolute ms-6"></i>
<input name="search" data-i18n="[placeholder]general.search" type="text" data-table-filter="search"
class="form-control rounded-1 w-250px ps-15 me-5" placeholder="Search" />
</div>
<div class="d-flex justify-content-end my-2" data-table-toolbar="base">
{{- if .LoggedUser.HasPermission "manage_event_rules"}}
<a href="{{.EventRuleURL}}" class="btn btn-primary ms-5">
<i class="ki-duotone ki-plus fs-2"></i>
<span data-i18n="general.add">Add</span>
</a>
{{- end}}
</div>
</div>
<table id="dataTable" class="table align-middle table-row-dashed fs-6 gy-5">
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Status</th>
<th>Description</th>
<th>Trigger</th>
<th>Actions</th>
<tr class="text-start text-muted fw-bold fs-6 gs-0">
<th data-i18n="general.name">Name</th>
<th data-i18n="general.status">Status</th>
<th data-i18n="rules.trigger">Trigger</th>
<th data-i18n="title.event_actions">Actions</th>
<th class="min-w-100px"></th>
</tr>
</thead>
<tbody>
{{range .Rules}}
<tr>
<td>{{.Trigger}}</td>
<td>{{.Name}}</td>
<td>{{if eq .Status 1 }}Active{{else}}Inactive{{end}}</td>
<td>{{.Description}}</td>
<td>{{.GetTriggerAsString}}</td>
<td>{{.GetActionsAsString}}</td>
</tr>
{{end}}
</tbody>
<tbody id="table_body" class="text-gray-800 fw-semibold"></tbody>
</table>
</div>
</div>
</div>
{{end}}
{{- end}}
{{define "dialog"}}
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">
Confirmation required
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">Do you want to delete the selected rule?</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">
Cancel
</button>
<a class="btn btn-warning" href="#" onclick="deleteAction()">
Delete
</a>
</div>
</div>
</div>
</div>
{{- define "extra_js"}}
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js"></script>
<script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
function deleteAction(name) {
ModalAlert.fire({
text: $.t('general.delete_confirm_generic'),
icon: "warning",
confirmButtonText: $.t('general.delete_confirm_btn'),
cancelButtonText: $.t('general.cancel'),
customClass: {
confirmButton: "btn btn-danger",
cancelButton: 'btn btn-secondary'
}
}).then((result) => {
if (result.isConfirmed){
$('#loading_message').text("");
KTApp.showPageLoading();
let path = '{{.EventRuleURL}}' + "/" + encodeURIComponent(name);
<div class="modal fade" id="runModal" tabindex="-1" role="dialog" aria-labelledby="runModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="runModalLabel">
Confirmation required
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">Do you want to execute the selected rule?</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">
Cancel
</button>
<a class="btn btn-warning" href="#" onclick="runAction()">
Run
</a>
</div>
</div>
</div>
</div>
{{end}}
{{define "extra_js"}}
<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.buttons.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/buttons.colVis.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.fixedHeader.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.select.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/ellipsis.js"></script>
<script type="text/javascript">
function runAction(){
let table = $('#dataTable').DataTable();
table.button('run:name').enable(false);
let name = table.row({ selected: true }).data()[1];
let path = '{{.EventRuleURL}}' + "/run/" + fixedEncodeURIComponent(name);
$('#runModal').modal('hide');
$('#successMsg').hide();
$('#errorMsg').hide();
$.ajax({
url: path,
type: 'POST',
dataType: 'json',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
timeout: 15000,
success: function (result) {
$('#successTxt').text("Rule actions started");
$('#successMsg').show();
setTimeout(function () {
$('#successMsg').hide();
}, 8000);
},
error: function ($xhr, textStatus, errorThrown) {
var txt = "Unable to run the selected rule";
if ($xhr) {
var json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
axios.delete(path, {
timeout: 15000,
headers: {
'X-CSRF-TOKEN': '{{.CSRFToken}}'
},
validateStatus: function (status) {
return status == 200;
}
}).then(function(response){
location.reload();
}).catch(function(error){
KTApp.hidePageLoading();
let errorMessage;
if (error && error.response) {
switch (error.response.status) {
case 403:
errorMessage = "general.delete_error_403";
break;
case 404:
errorMessage = "general.delete_error_404";
break;
}
}
}
$('#errorTxt').text(txt);
$('#errorMsg').show();
if (!errorMessage){
errorMessage = "general.delete_error_generic";
}
ModalAlert.fire({
text: $.t(errorMessage),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
});
}
});
}
function deleteAction() {
let table = $('#dataTable').DataTable();
table.button('delete:name').enable(false);
let name = table.row({ selected: true }).data()[1];
let path = '{{.EventRuleURL}}' + "/" + fixedEncodeURIComponent(name);
$('#deleteModal').modal('hide');
$('#errorMsg').hide();
function runAction(name) {
ModalAlert.fire({
text: $.t('rules.run_confirm'),
icon: "warning",
confirmButtonText: $.t('rules.run_confirm_btn'),
cancelButtonText: $.t('general.cancel'),
customClass: {
confirmButton: "btn btn-danger",
cancelButton: 'btn btn-secondary'
}
}).then((result) => {
if (result.isConfirmed){
$('#loading_message').text("");
KTApp.showPageLoading();
let path = '{{.EventRuleURL}}' + "/run/" + encodeURIComponent(name);
$.ajax({
url: path,
type: 'DELETE',
dataType: 'json',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
timeout: 15000,
success: function (result) {
window.location.href = '{{.EventRulesURL}}';
},
error: function ($xhr, textStatus, errorThrown) {
var txt = "Unable to delete the selected rule";
if ($xhr) {
var json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
}
axios.post(path, null, {
timeout: 15000,
headers: {
'X-CSRF-TOKEN': '{{.CSRFToken}}'
},
validateStatus: function (status) {
return status == 202;
}
}
$('#errorTxt').text(txt);
$('#errorMsg').show();
}).then(function(response){
KTApp.hidePageLoading();
ModalAlert.fire({
text: $.t("rules.run_ok"),
icon: "success",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
}).catch(function(error){
KTApp.hidePageLoading();
ModalAlert.fire({
text: $.t("rules.run_error_generic"),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
});
}
});
}
$(document).ready(function () {
$.fn.dataTable.ext.buttons.add = {
text: '<i class="fas fa-plus"></i>',
name: 'add',
titleAttr: "Add",
action: function (e, dt, node, config) {
window.location.href = '{{.EventRuleURL}}';
}
};
var datatable = function(){
var dt;
$.fn.dataTable.ext.buttons.edit = {
text: '<i class="fas fa-pen"></i>',
name: 'edit',
titleAttr: "Edit",
action: function (e, dt, node, config) {
let name = table.row({ selected: true }).data()[1];
let path = '{{.EventRuleURL}}' + "/" + fixedEncodeURIComponent(name);
window.location.href = path;
},
enabled: false
};
var initDatatable = function () {
$('#errorMsg').addClass("d-none");
dt = $('#dataTable').DataTable({
ajax: {
url: "{{.EventRulesURL}}/json",
dataSrc: "",
error: function ($xhr, textStatus, errorThrown) {
$(".dataTables_processing").hide();
let txt = "";
if ($xhr) {
let json = $xhr.responseJSON;
if (json) {
if (json.message){
txt = json.message;
}
}
}
if (!txt){
txt = "general.error500";
}
setI18NData($('#errorTxt'), txt);
$('#errorMsg').removeClass("d-none");
}
},
columns: [
{
data: "name",
render: function(data, type, row) {
if (type === 'display') {
return escapeHTML(data);
}
return data;
}
},
{
data: "status",
render: function(data, type, row) {
if (type === 'display') {
switch (data){
case 1:
return $.t('general.active');
default:
return $.t('general.inactive');
}
}
return data;
}
},
{
data: "trigger",
render: function(data, type, row) {
if (type === 'display') {
switch (data){
case 1:
return $.t('rules.triggers.fs_event');
case 2:
return $.t('rules.triggers.provider_event');
case 3:
return $.t('rules.triggers.schedule');
case 4:
return $.t('rules.triggers.ip_blocked');
case 5:
return $.t('rules.triggers.certificate_renewal');
case 6:
return $.t('rules.triggers.on_demand');
case 7:
return $.t('rules.triggers.idp_login');
default:
return "";
}
}
return data;
}
},
{
data: "actions",
defaultContent: [],
searchable: false,
orderable: false,
render: function(data, type, row) {
if (type === 'display') {
if (data){
let actions = [];
for (i = 0; i < data.length; i++){
actions.push(data[i].name);
}
return escapeHTML(actions.join(', '));
}
return ""
}
return "";
}
},
{
data: "id",
searchable: false,
orderable: false,
className: 'text-end',
render: function (data, type, row) {
if (type === 'display') {
let numActions = 0;
let actions = `<button class="btn btn-light btn-active-light-primary btn-flex btn-center btn-sm rotate" data-kt-menu-trigger="click" data-kt-menu-placement="bottom-end">
<span data-i18n="general.actions" class="fs-6">Actions</span>
<i class="ki-duotone ki-down fs-5 ms-1 rotate-180"></i>
</button>
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-6 w-200px py-4" data-kt-menu="true">`;
$.fn.dataTable.ext.buttons.delete = {
text: '<i class="fas fa-trash"></i>',
name: 'delete',
titleAttr: "Delete",
action: function (e, dt, node, config) {
$('#deleteModal').modal('show');
},
enabled: false
};
$.fn.dataTable.ext.buttons.run = {
text: '<i class="fas fa-play"></i>',
name: 'run',
titleAttr: "Run",
action: function (e, dt, node, config) {
$('#runModal').modal('show');
},
enabled: false
};
var table = $('#dataTable').DataTable({
"select": {
"style": "single",
"blurable": true
},
"stateSave": true,
"stateDuration": 0,
"buttons": [
{
"text": "Column visibility",
"extend": "colvis",
"columns": ":not(.noVis)"
//{{- if .LoggedUser.HasPermission "manage_event_rules"}}
numActions++;
actions+=`<div class="menu-item px-3">
<a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
</div>`
numActions++;
if (row.trigger === 6){
actions+=`<div class="menu-item px-3">
<a data-i18n="rules.run" href="#" class="menu-link px-3" data-table-action="run_row">Run</a>
</div>`
numActions++;
}
actions+=`<div class="menu-item px-3">
<a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
</div>`
//{{- end}}
if (numActions > 0){
actions+=`</div>`;
return actions;
}
}
return "";
}
},
],
deferRender: true,
stateSave: true,
stateDuration: 0,
stateLoadParams: function (settings, data) {
if (data.search.search){
const filterSearch = document.querySelector('[data-table-filter="search"]');
filterSearch.value = data.search.search;
}
},
language: {
info: $.t('datatable.info'),
infoEmpty: $.t('datatable.info_empty'),
infoFiltered: $.t('datatable.info_filtered'),
loadingRecords: "",
processing: $.t('datatable.processing'),
zeroRecords: "",
emptyTable: $.t('datatable.no_records')
},
order: [[0, 'asc']],
initComplete: function(settings, json) {
$('#loader').addClass("d-none");
$('#card_content').removeClass("d-none");
let api = $.fn.dataTable.Api(settings);
api.columns.adjust().draw("page");
drawAction();
}
],
"columnDefs": [
{
"targets": [0],
"visible": false,
"searchable": false,
"className": "noVis"
},
{
"targets": [1,2],
"className": "noVis"
},
{
"targets": [3],
"visible": false
},
{
"targets": [5],
"render": $.fn.dataTable.render.ellipsis(100, true)
},
],
"scrollX": false,
"scrollY": false,
"responsive": true,
"language": {
"emptyTable": "No event rules defined"
},
"order": [[1, 'asc']]
});
});
new $.fn.dataTable.FixedHeader( table );
dt.on('draw', drawAction);
dt.on('column-reorder', function(e, settings, details){
drawAction();
});
}
table.button().add(0,'run');
table.button().add(0,'delete');
table.button().add(0,'edit');
table.button().add(0,'add');
function drawAction() {
KTMenu.createInstances();
handleRowActions();
$('#table_body').localize();
}
table.buttons().container().appendTo('.col-md-6:eq(0)', table.table().container());
var handleDatatableActions = function () {
const filterSearch = $(document.querySelector('[data-table-filter="search"]'));
filterSearch.off("keyup");
filterSearch.on('keyup', function (e) {
dt.rows().deselect();
dt.search(e.target.value, true, false).draw();
});
}
table.on('select deselect', function () {
var selectedRows = table.rows({ selected: true }).count();
table.button('delete:name').enable(selectedRows == 1);
table.button('edit:name').enable(selectedRows == 1);
if (selectedRows == 1){
table.button('run:name').enable(table.row({ selected: true }).data()[0] == 6);
} else {
table.button('run:name').enable(false);
function handleRowActions() {
const editButtons = document.querySelectorAll('[data-table-action="edit_row"]');
editButtons.forEach(d => {
let el = $(d);
el.off("click");
el.on("click", function(e){
e.preventDefault();
let rowData = dt.row(e.target.closest('tr')).data();
window.location.replace('{{.EventRuleURL}}' + "/" + encodeURIComponent(rowData['name']));
});
});
const deleteButtons = document.querySelectorAll('[data-table-action="delete_row"]');
deleteButtons.forEach(d => {
let el = $(d);
el.off("click");
el.on("click", function(e){
e.preventDefault();
const parent = e.target.closest('tr');
deleteAction(dt.row(parent).data()['name']);
});
});
const runButtons = document.querySelectorAll('[data-table-action="run_row"]');
runButtons.forEach(d => {
let el = $(d);
el.off("click");
el.on("click", function(e){
e.preventDefault();
const parent = e.target.closest('tr');
runAction(dt.row(parent).data()['name']);
});
});
}
return {
init: function () {
initDatatable();
handleDatatableActions();
}
});
}
}();
$(document).on("i18nshow", function(){
datatable.init();
});
</script>
{{end}}
{{- end}}