2FA (TOTP): - backend/api/auth.go: zweistufiger Login-Flow mit Pending-Token (5min), Endpunkte /2fa/validate, /2fa/setup, /2fa/confirm, /2fa/disable - backend/db/users.go: GetUserByID, SetTOTPSecret, EnableTOTP, DisableTOTP - backend/db/postgres.go: Migration totp_secret + totp_enabled Spalten - backend/models/user.go: TOTPEnabled/TOTPSecret Felder, neue Request-Typen - backend/main.go: neue Routen registriert (/2fa/validate ohne Auth, rest geschuetzt) - backend/go.mod+sum: github.com/pquerna/otp hinzugefuegt - frontend/src/pages/Login.jsx: zweistufiger Login (Passwort -> TOTP) - frontend/src/pages/SettingsPage.jsx: TwoFactorSection mit Setup/Deaktivieren - frontend/src/stores/auth.js: validateTOTP Action - frontend/src/api/client.js: setupTOTP, confirmTOTP, disableTOTP, validateTOTP Weitere Aenderungen (unstaged -> jetzt committed): - frontend/src/components/ScriptModal.jsx: neu - frontend/src/components/AgentPanel.jsx: Erweiterungen - frontend/src/components/ProxmoxPanel.jsx: Erweiterungen - agent-linux/ws/handler.go: Erweiterungen - backend/api/scheduler.go + tasks.go: Erweiterungen - backend/db/tasks.go + models/task.go: Erweiterungen
292 lines
8.2 KiB
Go
292 lines
8.2 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/cynfo/rmm-backend/ws"
|
|
)
|
|
|
|
// GET /api/v1/tasks
|
|
func (h *Handler) listTasks(w http.ResponseWriter, r *http.Request) {
|
|
limit := 50
|
|
offset := 0
|
|
agentID := r.URL.Query().Get("agent_id")
|
|
status := r.URL.Query().Get("status")
|
|
action := r.URL.Query().Get("action")
|
|
|
|
if l := r.URL.Query().Get("limit"); l != "" {
|
|
if n, err := strconv.Atoi(l); err == nil && n > 0 {
|
|
limit = n
|
|
}
|
|
}
|
|
if o := r.URL.Query().Get("offset"); o != "" {
|
|
if n, err := strconv.Atoi(o); err == nil && n >= 0 {
|
|
offset = n
|
|
}
|
|
}
|
|
|
|
tasks, total, err := h.db.GetTasks(limit, offset, agentID, status, action)
|
|
if err != nil {
|
|
log.Printf("Tasks laden fehlgeschlagen: %v", err)
|
|
writeError(w, http.StatusInternalServerError, "Fehler beim Laden")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"data": tasks,
|
|
"total": total,
|
|
})
|
|
}
|
|
|
|
// GET /api/v1/tasks/{id}
|
|
func (h *Handler) getTask(w http.ResponseWriter, r *http.Request) {
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungueltige Task-ID")
|
|
return
|
|
}
|
|
|
|
task, err := h.db.GetTask(id)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Fehler beim Laden")
|
|
return
|
|
}
|
|
if task == nil {
|
|
writeError(w, http.StatusNotFound, "Task nicht gefunden")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, task)
|
|
}
|
|
|
|
// POST /api/v1/tasks
|
|
func (h *Handler) createTask(hub *ws.Hub) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
AgentID string `json:"agent_id"`
|
|
Name string `json:"name"`
|
|
Action string `json:"action"`
|
|
ScheduledAt string `json:"scheduled_at"`
|
|
ScheduleType string `json:"schedule_type"`
|
|
ScheduleTime string `json:"schedule_time"`
|
|
ScheduleWeekday *int `json:"schedule_weekday"`
|
|
ScheduleMonthday *int `json:"schedule_monthday"`
|
|
Reboot bool `json:"reboot"`
|
|
HealthCheck bool `json:"health_check"`
|
|
HealthCheckTimeout int `json:"health_check_timeout"`
|
|
SecurityOnly bool `json:"security_only"`
|
|
Script string `json:"script"`
|
|
ScriptTimeout int `json:"script_timeout"`
|
|
WebhookURL string `json:"webhook_url"`
|
|
Active *bool `json:"active"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungueltige Anfrage")
|
|
return
|
|
}
|
|
|
|
if req.AgentID == "" || req.Action == "" {
|
|
writeError(w, http.StatusBadRequest, "agent_id und action sind erforderlich")
|
|
return
|
|
}
|
|
|
|
if req.Action != "backup" && req.Action != "update" && req.Action != "zpoolscrub" && req.Action != "script" {
|
|
writeError(w, http.StatusBadRequest, "Ungueltige Aktion (backup, update, zpoolscrub, script)")
|
|
return
|
|
}
|
|
if req.Action == "script" && req.Script == "" {
|
|
writeError(w, http.StatusBadRequest, "Skript-Inhalt darf nicht leer sein")
|
|
return
|
|
}
|
|
|
|
agent, _, err := h.db.GetAgent(req.AgentID)
|
|
if err != nil || agent == nil {
|
|
writeError(w, http.StatusNotFound, "Agent nicht gefunden")
|
|
return
|
|
}
|
|
|
|
createdBy := "api-key"
|
|
if claims, ok := r.Context().Value(UserContextKey).(*JWTClaims); ok {
|
|
createdBy = claims.Username
|
|
}
|
|
|
|
active := true
|
|
if req.Active != nil {
|
|
active = *req.Active
|
|
}
|
|
|
|
task, err := h.db.CreateTask(
|
|
req.AgentID, req.Action, req.ScheduledAt, req.WebhookURL, createdBy,
|
|
req.Name, req.ScheduleType, req.ScheduleTime,
|
|
req.ScheduleWeekday, req.ScheduleMonthday,
|
|
req.Reboot, req.HealthCheck, req.HealthCheckTimeout, req.SecurityOnly, active,
|
|
req.Script, req.ScriptTimeout,
|
|
)
|
|
if err != nil {
|
|
log.Printf("Task erstellen fehlgeschlagen: %v", err)
|
|
writeError(w, http.StatusInternalServerError, "Task erstellen fehlgeschlagen: "+err.Error())
|
|
return
|
|
}
|
|
|
|
h.auditLog(r, "task.create", "task", strconv.Itoa(task.ID), agent.Name, req.Action)
|
|
|
|
// If once and scheduled_at is in the past or now, scheduler picks it up within 30s
|
|
if req.ScheduledAt != "" && req.ScheduleType == "once" {
|
|
if schedTime, err := time.Parse(time.RFC3339, req.ScheduledAt); err == nil && !schedTime.After(time.Now()) {
|
|
// scheduler will pick it up
|
|
_ = schedTime
|
|
}
|
|
}
|
|
|
|
writeJSON(w, http.StatusCreated, task)
|
|
}
|
|
}
|
|
|
|
// PUT /api/v1/tasks/{id}/toggle
|
|
func (h *Handler) toggleTask(w http.ResponseWriter, r *http.Request) {
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungueltige Task-ID")
|
|
return
|
|
}
|
|
|
|
task, err := h.db.ToggleTask(id)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Fehler beim Umschalten")
|
|
return
|
|
}
|
|
if task == nil {
|
|
writeError(w, http.StatusNotFound, "Task nicht gefunden")
|
|
return
|
|
}
|
|
|
|
h.auditLog(r, "task.toggle", "task", idStr, "", "")
|
|
writeJSON(w, http.StatusOK, task)
|
|
}
|
|
|
|
// POST /api/v1/tasks/{id}/cancel
|
|
func (h *Handler) cancelTask(w http.ResponseWriter, r *http.Request) {
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungueltige Task-ID")
|
|
return
|
|
}
|
|
|
|
if err := h.db.CancelTask(id); err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
h.auditLog(r, "task.cancel", "task", idStr, "", "")
|
|
writeJSON(w, http.StatusOK, map[string]string{"message": "Task abgebrochen"})
|
|
}
|
|
|
|
// PUT /api/v1/tasks/{id}
|
|
func (h *Handler) updateTask(w http.ResponseWriter, r *http.Request) {
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungueltige Task-ID")
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
ScheduleType string `json:"schedule_type"`
|
|
ScheduleTime string `json:"schedule_time"`
|
|
ScheduleWeekday *int `json:"schedule_weekday"`
|
|
ScheduleMonthday *int `json:"schedule_monthday"`
|
|
Reboot bool `json:"reboot"`
|
|
HealthCheck bool `json:"health_check"`
|
|
HealthCheckTimeout int `json:"health_check_timeout"`
|
|
WebhookURL string `json:"webhook_url"`
|
|
Active *bool `json:"active"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungueltige Anfrage")
|
|
return
|
|
}
|
|
|
|
active := true
|
|
if req.Active != nil {
|
|
active = *req.Active
|
|
}
|
|
if req.HealthCheckTimeout <= 0 {
|
|
req.HealthCheckTimeout = 600
|
|
}
|
|
|
|
task, err := h.db.UpdateTask(id, req.Name, req.ScheduleType, req.ScheduleTime,
|
|
req.ScheduleWeekday, req.ScheduleMonthday,
|
|
req.Reboot, req.HealthCheck, req.HealthCheckTimeout, req.WebhookURL, active)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "Fehler: "+err.Error())
|
|
return
|
|
}
|
|
if task == nil {
|
|
writeError(w, http.StatusNotFound, "Task nicht gefunden")
|
|
return
|
|
}
|
|
|
|
h.auditLog(r, "task.update", "task", idStr, "", "")
|
|
writeJSON(w, http.StatusOK, task)
|
|
}
|
|
|
|
// SetScheduler setzt den Scheduler im Handler (fuer Run-Now)
|
|
func (h *Handler) SetScheduler(s *TaskScheduler) {
|
|
h.scheduler = s
|
|
}
|
|
|
|
// POST /api/v1/tasks/{id}/run — Task sofort ausfuehren
|
|
func (h *Handler) runTaskNow() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungueltige Task-ID")
|
|
return
|
|
}
|
|
|
|
task, err := h.db.GetTask(id)
|
|
if err != nil || task == nil {
|
|
writeError(w, http.StatusNotFound, "Task nicht gefunden")
|
|
return
|
|
}
|
|
|
|
if h.scheduler == nil {
|
|
writeError(w, http.StatusInternalServerError, "Scheduler nicht verfuegbar")
|
|
return
|
|
}
|
|
|
|
// Sofort im Hintergrund starten
|
|
go h.scheduler.executeTask(*task)
|
|
|
|
h.auditLog(r, "task.run_now", "task", idStr, "", task.Action)
|
|
writeJSON(w, http.StatusOK, map[string]string{"message": "Task gestartet"})
|
|
}
|
|
}
|
|
|
|
// DELETE /api/v1/tasks/{id}
|
|
func (h *Handler) deleteTask(w http.ResponseWriter, r *http.Request) {
|
|
idStr := r.PathValue("id")
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungueltige Task-ID")
|
|
return
|
|
}
|
|
|
|
if err := h.db.DeleteTask(id); err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
h.auditLog(r, "task.delete", "task", idStr, "", "")
|
|
writeJSON(w, http.StatusOK, map[string]string{"message": "Task geloescht"})
|
|
}
|