package api import ( "encoding/json" "net/http" "strings" "github.com/go-chi/chi/v5" "github.com/sandrodach/mindsift/internal/auth" "github.com/sandrodach/mindsift/internal/domain" ) // --- ORGANIZATIONS & TEAMS --- func (s *Server) handleAdminGetOrganizations(w http.ResponseWriter, r *http.Request) { orgs, err := s.store.GetOrganizations() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, orgs) } func (s *Server) handleAdminCreateOrganization(w http.ResponseWriter, r *http.Request) { var req struct { Name string `json:"name"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "Ungültiges JSON") return } org, err := s.store.CreateOrganization(req.Name) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusCreated, org) } func (s *Server) handleAdminRenameOrganization(w http.ResponseWriter, r *http.Request) { var req struct { Name string `json:"name"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "Ungültiges JSON") return } if err := s.store.RenameOrganization(chi.URLParam(r, "id"), req.Name); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]bool{"updated": true}) } func (s *Server) handleAdminDeleteOrganization(w http.ResponseWriter, r *http.Request) { if err := s.store.DeleteOrganization(chi.URLParam(r, "id")); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]bool{"deleted": true}) } func (s *Server) handleAdminSetUserOrganization(w http.ResponseWriter, r *http.Request) { var req struct { OrganizationID string `json:"organizationId"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "Ungültiges JSON") return } if err := s.store.SetUserOrganization(chi.URLParam(r, "id"), req.OrganizationID); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]bool{"updated": true}) } // handleAdminSharingImpact counts the shares and group memberships a switch to ?mode= would remove. func (s *Server) handleAdminSharingImpact(w http.ResponseWriter, r *http.Request) { n, err := s.store.PruneSharing(r.URL.Query().Get("mode"), true) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]int64{"affected": n}) } // --- ADMIN & STAFF PANEL --- func (s *Server) handleAdminGetUsers(w http.ResponseWriter, r *http.Request) { users, err := s.store.GetAllUsers() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, users) } func (s *Server) handleAdminCreateUser(w http.ResponseWriter, r *http.Request) { callerID := auth.GetUserID(r.Context()) caller, _ := s.store.GetUserByID(callerID) var req struct { Email string `json:"email"` Password string `json:"password"` DisplayName string `json:"displayName"` Role string `json:"role"` OrganizationID string `json:"organizationId"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "Ungültiges JSON") return } if req.Email == "" { writeError(w, http.StatusBadRequest, "E-Mail-Adresse erforderlich") return } if err := auth.ValidatePassword(req.Password); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } // Only full server admin can create another server admin if req.Role == "admin" && (caller == nil || caller.Role != "admin") { req.Role = "member" } if strings.TrimSpace(req.DisplayName) == "" { req.DisplayName = strings.Split(req.Email, "@")[0] } hash, err := auth.HashPassword(req.Password) if err != nil { writeError(w, http.StatusInternalServerError, "Passwort-Hash fehlgeschlagen") return } u, err := s.store.CreateUser(req.Email, hash, req.DisplayName, req.Role) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } if req.OrganizationID != "" { if err := s.store.SetUserOrganization(u.ID, req.OrganizationID); err == nil { u.OrganizationID = req.OrganizationID } } writeJSON(w, http.StatusCreated, u) } func (s *Server) handleAdminUpdateUser(w http.ResponseWriter, r *http.Request) { callerID := auth.GetUserID(r.Context()) caller, _ := s.store.GetUserByID(callerID) targetUserID := chi.URLParam(r, "id") targetUser, err := s.store.GetUserByID(targetUserID) if err != nil || targetUser == nil { writeError(w, http.StatusNotFound, "Benutzer nicht gefunden") return } // Team-Admins cannot modify Server-Admins if caller != nil && caller.Role != "admin" && targetUser.Role == "admin" { writeError(w, http.StatusForbidden, "Nur Server-Administratoren können Server-Admin-Konten bearbeiten") return } var req struct { DisplayName *string `json:"displayName"` Email *string `json:"email"` Role *string `json:"role"` IsActive *bool `json:"isActive"` DiscordWebhookURL *string `json:"discordWebhookUrl"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "Ungültiges JSON") return } if req.Role != nil && *req.Role == "admin" && (caller == nil || caller.Role != "admin") { writeError(w, http.StatusForbidden, "Nur Server-Administratoren können die Rolle Server-Admin vergeben") return } if req.DiscordWebhookURL != nil { if err := validateWebhookURL(*req.DiscordWebhookURL, true, s.allowPrivateWebhooks()); err != nil { writeError(w, http.StatusBadRequest, "Discord-Webhook: "+err.Error()) return } } u, err := s.store.UpdateUser(targetUserID, req.DisplayName, req.Email, req.Role, req.IsActive, req.DiscordWebhookURL, nil) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } if req.IsActive != nil && !*req.IsActive { s.wsHub.DisconnectUser(targetUserID) } writeJSON(w, http.StatusOK, u) } func (s *Server) handleAdminResetPassword(w http.ResponseWriter, r *http.Request) { callerID := auth.GetUserID(r.Context()) caller, _ := s.store.GetUserByID(callerID) targetUserID := chi.URLParam(r, "id") targetUser, _ := s.store.GetUserByID(targetUserID) if caller != nil && caller.Role != "admin" && targetUser != nil && targetUser.Role == "admin" { writeError(w, http.StatusForbidden, "Keine Berechtigung") return } var req struct { Password string `json:"password"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "Neues Passwort erforderlich") return } if err := auth.ValidatePassword(req.Password); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } hash, err := auth.HashPassword(req.Password) if err != nil { writeError(w, http.StatusInternalServerError, "Passwort-Hash fehlgeschlagen") return } s.loginGuard.unlockAccount(targetUserID) if err := s.store.UpdateUserPassword(targetUserID, hash); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } s.wsHub.DisconnectUser(targetUserID) writeJSON(w, http.StatusOK, map[string]bool{"success": true}) } func (s *Server) handleAdminDeleteUser(w http.ResponseWriter, r *http.Request) { targetUserID := chi.URLParam(r, "id") currUserID := auth.GetUserID(r.Context()) if targetUserID == currUserID { writeError(w, http.StatusBadRequest, "Sie können Ihren eigenen Benutzer nicht löschen") return } caller, _ := s.store.GetUserByID(currUserID) targetUser, _ := s.store.GetUserByID(targetUserID) if caller != nil && caller.Role != "admin" && targetUser != nil && targetUser.Role == "admin" { writeError(w, http.StatusForbidden, "Keine Berechtigung zum Löschen eines Server-Admins") return } if err := s.store.DeleteUser(targetUserID); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]bool{"deleted": true}) } // --- INVITES MANAGEMENT --- func (s *Server) handleAdminGetInvites(w http.ResponseWriter, r *http.Request) { invites, err := s.store.GetUserInvites() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, invites) } func (s *Server) handleAdminCreateInvite(w http.ResponseWriter, r *http.Request) { callerID := auth.GetUserID(r.Context()) caller, _ := s.store.GetUserByID(callerID) var req struct { Email string `json:"email"` Role string `json:"role"` OrganizationID string `json:"organizationId"` ValidDays int `json:"validDays"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "Ungültiges JSON") return } if req.Role == "admin" && (caller == nil || caller.Role != "admin") { req.Role = "member" } inv, err := s.store.CreateUserInvite(callerID, req.Email, req.Role, req.OrganizationID, req.ValidDays) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusCreated, inv) } func (s *Server) handleAdminDeleteInvite(w http.ResponseWriter, r *http.Request) { inviteID := chi.URLParam(r, "id") if err := s.store.DeleteUserInvite(inviteID); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]bool{"deleted": true}) } // --- SERVER SETTINGS & SMTP --- func (s *Server) handleAdminGetSettings(w http.ResponseWriter, r *http.Request) { settings, err := s.store.GetSystemSettings() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } settings.SMTPConfiguredViaEnv = strings.TrimSpace(s.cfg.SMTPHost) != "" writeJSON(w, http.StatusOK, settings) } func (s *Server) handleAdminUpdateSettings(w http.ResponseWriter, r *http.Request) { var settings domain.SystemSettings if err := json.NewDecoder(r.Body).Decode(&settings); err != nil { writeError(w, http.StatusBadRequest, "Ungültiges JSON") return } previousMode := s.store.SharingMode() if err := s.store.UpdateSystemSettings(&settings); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } // A stricter mode takes away what is no longer allowed (shares, group memberships, assignments) if settings.SharingMode != previousMode { if _, err := s.store.PruneSharing(settings.SharingMode, false); err != nil { writeError(w, http.StatusInternalServerError, "Freigaben konnten nicht angepasst werden: "+err.Error()) return } } settings.SMTPConfiguredViaEnv = strings.TrimSpace(s.cfg.SMTPHost) != "" writeJSON(w, http.StatusOK, settings) } func (s *Server) handleAdminTestSMTP(w http.ResponseWriter, r *http.Request) { userID := auth.GetUserID(r.Context()) user, _ := s.store.GetUserByID(userID) var req struct { ToEmail string `json:"toEmail"` } _ = json.NewDecoder(r.Body).Decode(&req) toEmail := strings.TrimSpace(req.ToEmail) if toEmail == "" && user != nil { toEmail = user.Email } if toEmail == "" { writeError(w, http.StatusBadRequest, "Ziel-E-Mail-Adresse erforderlich") return } if err := s.sendEmailNotification(toEmail, "MindSift SMTP-Server Test", "Die SMTP-Konfiguration deiner MindSift-Instanz funktioniert einwandfrei!"); err != nil { writeError(w, http.StatusBadGateway, "SMTP-Fehler: "+err.Error()) return } writeJSON(w, http.StatusOK, map[string]interface{}{ "sent": true, "toEmail": toEmail, }) }