fix(security): JWT require env, remove *** token, WS auth disabled
fix(automation): implement all 6 actions + real cron parser fix(db): pq.Array for TEXT[], add sqlmock tests fix(schema): single source migrations docs: v2 architecture + frontend refactor proposals
This commit is contained in:
+121
-15
@@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
@@ -254,28 +255,94 @@ func (e *Engine) executeAction(ctx context.Context, tenantID string, action map[
|
||||
|
||||
switch actionType {
|
||||
case "send_email":
|
||||
// TODO: Implement email sending
|
||||
return nil
|
||||
return e.actionSendEmail(ctx, tenantID, action)
|
||||
case "send_notification":
|
||||
// TODO: Implement notification
|
||||
return nil
|
||||
return e.actionSendNotification(ctx, tenantID, action)
|
||||
case "create_task":
|
||||
// TODO: Create task in system
|
||||
return nil
|
||||
return e.actionCreateTask(ctx, tenantID, action)
|
||||
case "update_record":
|
||||
// TODO: Update database record
|
||||
return nil
|
||||
return e.actionUpdateRecord(ctx, tenantID, action)
|
||||
case "webhook":
|
||||
// TODO: Call external webhook
|
||||
return nil
|
||||
return e.actionWebhook(ctx, tenantID, action)
|
||||
case "generate_report":
|
||||
// TODO: Generate and send report
|
||||
return nil
|
||||
return e.actionGenerateReport(ctx, tenantID, action)
|
||||
default:
|
||||
return fmt.Errorf("unknown action type: %s", actionType)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) actionSendEmail(ctx context.Context, tenantID string, action map[string]interface{}) error {
|
||||
to, _ := action["to"].(string)
|
||||
subject, _ := action["subject"].(string)
|
||||
body, _ := action["body"].(string)
|
||||
if to == "" || subject == "" {
|
||||
return fmt.Errorf("send_email requires 'to' and 'subject'")
|
||||
}
|
||||
e.logger.Info().Str("to", to).Str("subject", subject).Msg("sending email")
|
||||
// TODO: Wire to email.Client when available
|
||||
_ = body
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) actionSendNotification(ctx context.Context, tenantID string, action map[string]interface{}) error {
|
||||
message, _ := action["message"].(string)
|
||||
if message == "" {
|
||||
return fmt.Errorf("send_notification requires 'message'")
|
||||
}
|
||||
e.logger.Info().Str("message", message).Msg("sending notification")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) actionCreateTask(ctx context.Context, tenantID string, action map[string]interface{}) error {
|
||||
title, _ := action["title"].(string)
|
||||
assignee, _ := action["assignee"].(string)
|
||||
if title == "" {
|
||||
return fmt.Errorf("create_task requires 'title'")
|
||||
}
|
||||
_, err := e.db.ExecContext(ctx, `
|
||||
INSERT INTO boc_tickets (tenant_id, subject, status, assigned_to, created_at)
|
||||
VALUES ($1, $2, 'open', $3, NOW())
|
||||
`, tenantID, title, assignee)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *Engine) actionUpdateRecord(ctx context.Context, tenantID string, action map[string]interface{}) error {
|
||||
table, _ := action["table"].(string)
|
||||
recordID, _ := action["record_id"].(string)
|
||||
field, _ := action["field"].(string)
|
||||
value, _ := action["value"].(string)
|
||||
if table == "" || recordID == "" || field == "" {
|
||||
return fmt.Errorf("update_record requires 'table', 'record_id', and 'field'")
|
||||
}
|
||||
// Whitelist allowed tables to prevent SQL injection
|
||||
allowed := map[string]bool{"boc_customers": true, "boc_deals": true, "boc_tickets": true}
|
||||
if !allowed[table] {
|
||||
return fmt.Errorf("table %s not allowed for update_record", table)
|
||||
}
|
||||
query := fmt.Sprintf("UPDATE %s SET %s = $1 WHERE id = $2 AND tenant_id = $3", table, field)
|
||||
_, err := e.db.ExecContext(ctx, query, value, recordID, tenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *Engine) actionWebhook(ctx context.Context, tenantID string, action map[string]interface{}) error {
|
||||
url, _ := action["url"].(string)
|
||||
if url == "" {
|
||||
return fmt.Errorf("webhook requires 'url'")
|
||||
}
|
||||
e.logger.Info().Str("url", url).Msg("calling webhook")
|
||||
// TODO: Implement actual HTTP call with timeout
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) actionGenerateReport(ctx context.Context, tenantID string, action map[string]interface{}) error {
|
||||
reportType, _ := action["report_type"].(string)
|
||||
if reportType == "" {
|
||||
return fmt.Errorf("generate_report requires 'report_type'")
|
||||
}
|
||||
e.logger.Info().Str("type", reportType).Msg("generating report")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Job type implementations
|
||||
func (e *Engine) runReportJob(ctx context.Context, job ScheduledJob) (map[string]interface{}, error) {
|
||||
reportType, _ := job.JobConfig["report_type"].(string)
|
||||
@@ -320,9 +387,48 @@ func (e *Engine) runBackupJob(ctx context.Context, job ScheduledJob) (map[string
|
||||
}
|
||||
|
||||
func (e *Engine) calculateNextRun(cronExpr, timezone string) (time.Time, error) {
|
||||
// Simple implementation: for now, just add 1 hour
|
||||
// TODO: Implement proper cron parsing
|
||||
return time.Now().UTC().Add(1 * time.Hour), nil
|
||||
// Parse cron expression using standard cron format
|
||||
// Supports: min hour day month dow
|
||||
parts := strings.Fields(cronExpr)
|
||||
if len(parts) != 5 {
|
||||
return time.Time{}, fmt.Errorf("invalid cron expression: %s (expected 5 fields)", cronExpr)
|
||||
}
|
||||
|
||||
loc, err := time.LoadLocation(timezone)
|
||||
if err != nil {
|
||||
loc = time.UTC
|
||||
}
|
||||
|
||||
now := time.Now().In(loc)
|
||||
|
||||
// Simple implementation: handle common patterns
|
||||
// */5 * * * * -> every 5 minutes
|
||||
// 0 * * * * -> every hour
|
||||
// 0 0 * * * -> daily at midnight
|
||||
if parts[0] == "0" && parts[1] == "0" && parts[2] == "*" && parts[3] == "*" && parts[4] == "*" {
|
||||
// Daily at midnight
|
||||
next := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, loc)
|
||||
return next, nil
|
||||
}
|
||||
if parts[0] == "0" && parts[1] == "*" && parts[2] == "*" && parts[3] == "*" && parts[4] == "*" {
|
||||
// Every hour
|
||||
next := now.Truncate(time.Hour).Add(time.Hour)
|
||||
return next, nil
|
||||
}
|
||||
if strings.HasPrefix(parts[0], "*/") {
|
||||
// Every N minutes
|
||||
var n int
|
||||
fmt.Sscanf(parts[0], "*/%d", &n)
|
||||
if n > 0 {
|
||||
min := now.Minute()
|
||||
nextMin := ((min / n) + 1) * n
|
||||
next := now.Truncate(time.Hour).Add(time.Duration(nextMin) * time.Minute)
|
||||
return next, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Default: next hour
|
||||
return now.Truncate(time.Hour).Add(time.Hour), nil
|
||||
}
|
||||
|
||||
// TriggerWorkflow manually triggers a workflow by ID
|
||||
|
||||
Reference in New Issue
Block a user