MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 16s
1335 lines
41 KiB
Go
1335 lines
41 KiB
Go
package api
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io/fs"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/go-chi/chi/v5"
|
||
"github.com/go-chi/chi/v5/middleware"
|
||
"github.com/go-chi/cors"
|
||
"github.com/sandrodach/mindsift/internal/auth"
|
||
"github.com/sandrodach/mindsift/internal/config"
|
||
"github.com/sandrodach/mindsift/internal/domain"
|
||
"github.com/sandrodach/mindsift/internal/storage"
|
||
"github.com/sandrodach/mindsift/internal/ws"
|
||
)
|
||
|
||
type Server struct {
|
||
cfg *config.Config
|
||
store *storage.Store
|
||
wsHub *ws.Hub
|
||
staticFS fs.FS
|
||
}
|
||
|
||
func NewServer(cfg *config.Config, store *storage.Store, wsHub *ws.Hub, staticFS fs.FS) *Server {
|
||
srv := &Server{
|
||
cfg: cfg,
|
||
store: store,
|
||
wsHub: wsHub,
|
||
staticFS: staticFS,
|
||
}
|
||
go srv.startDiscordAlarmTicker()
|
||
return srv
|
||
}
|
||
|
||
func (s *Server) Router() http.Handler {
|
||
r := chi.NewRouter()
|
||
|
||
r.Use(middleware.RealIP)
|
||
r.Use(middleware.Recoverer)
|
||
r.Use(cors.Handler(cors.Options{
|
||
AllowedOrigins: []string{"*"},
|
||
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-API-Key"},
|
||
ExposedHeaders: []string{"Link"},
|
||
AllowCredentials: true,
|
||
MaxAge: 300,
|
||
}))
|
||
|
||
// Public Health & Webhook Routes
|
||
r.Get("/api/v1/health", s.handleHealth)
|
||
r.Post("/api/v1/auth/register", s.handleRegister)
|
||
r.Post("/api/v1/auth/login", s.handleLogin)
|
||
r.Post("/api/v1/auth/quickstart", s.handleQuickStart)
|
||
r.Post("/api/v1/webhooks/ingest", s.handleWebhookIngest)
|
||
r.Get("/api/v1/ws/spaces/{spaceId}", s.handleWebSocket)
|
||
|
||
// Authenticated API Routes
|
||
r.Group(func(r chi.Router) {
|
||
r.Use(s.authMiddleware)
|
||
|
||
r.Get("/api/v1/auth/me", s.handleMe)
|
||
r.Patch("/api/v1/auth/settings", s.handleUpdateUserSettings)
|
||
r.Get("/api/v1/stats", s.handleStats)
|
||
r.Get("/api/v1/search", s.handleSearch)
|
||
r.Get("/api/v1/export", s.handleExport)
|
||
|
||
// Instant Capture & Thoughts
|
||
r.Post("/api/v1/inbox", s.handleCaptureInbox)
|
||
r.Get("/api/v1/thoughts", s.handleGetThoughts)
|
||
r.Patch("/api/v1/thoughts/{id}", s.handleUpdateThought)
|
||
r.Delete("/api/v1/thoughts/{id}", s.handleDeleteThought)
|
||
r.Delete("/api/v1/thoughts/discarded/purge", s.handlePurgeDiscarded)
|
||
|
||
// Tinder-Style Triage & Tasks
|
||
r.Get("/api/v1/triage/deck", s.handleGetTriageDeck)
|
||
r.Patch("/api/v1/triage/{id}", s.handleTriageAction)
|
||
r.Get("/api/v1/tasks", s.handleGetTasks)
|
||
|
||
// Task Lists
|
||
r.Get("/api/v1/task-lists", s.handleGetTaskLists)
|
||
r.Post("/api/v1/task-lists", s.handleCreateTaskList)
|
||
r.Put("/api/v1/task-lists/{id}", s.handleUpdateTaskList)
|
||
r.Delete("/api/v1/task-lists/{id}", s.handleDeleteTaskList)
|
||
|
||
// Workspace Documents
|
||
r.Get("/api/v1/documents", s.handleGetDocuments)
|
||
r.Post("/api/v1/documents", s.handleCreateDocument)
|
||
r.Get("/api/v1/documents/{id}", s.handleGetDocument)
|
||
r.Put("/api/v1/documents/{id}", s.handleUpdateDocument)
|
||
r.Delete("/api/v1/documents/{id}", s.handleDeleteDocument)
|
||
|
||
// Shared Spaces & Realtime Lists
|
||
r.Get("/api/v1/spaces", s.handleGetSpaces)
|
||
r.Post("/api/v1/spaces", s.handleCreateSpace)
|
||
r.Post("/api/v1/spaces/join", s.handleJoinSpace)
|
||
r.Get("/api/v1/spaces/{id}/lists", s.handleGetSpaceLists)
|
||
r.Post("/api/v1/spaces/{id}/lists", s.handleCreateList)
|
||
|
||
r.Get("/api/v1/lists", s.handleGetAllLists)
|
||
r.Delete("/api/v1/lists/{id}", s.handleDeleteList)
|
||
r.Post("/api/v1/lists/{id}/items", s.handleCreateListItem)
|
||
r.Patch("/api/v1/lists/{id}/items/{itemId}", s.handleToggleListItem)
|
||
r.Delete("/api/v1/lists/{id}/items/{itemId}", s.handleDeleteListItem)
|
||
r.Post("/api/v1/lists/{id}/clear-completed", s.handleClearCompletedItems)
|
||
|
||
// Organizations & Teams
|
||
r.Get("/api/v1/organizations", s.handleGetOrganizations)
|
||
r.Post("/api/v1/organizations", s.handleCreateOrganization)
|
||
r.Get("/api/v1/organizations/{id}/teams", s.handleGetOrgTeams)
|
||
r.Post("/api/v1/organizations/{id}/teams", s.handleCreateOrgTeam)
|
||
r.Get("/api/v1/teams", s.handleGetUserTeams)
|
||
r.Post("/api/v1/teams/{id}/members", s.handleAddTeamMember)
|
||
r.Delete("/api/v1/teams/{id}/members/{userId}", s.handleRemoveTeamMember)
|
||
|
||
// API Keys Management
|
||
r.Get("/api/v1/keys", s.handleGetAPIKeys)
|
||
r.Post("/api/v1/keys", s.handleCreateAPIKey)
|
||
r.Delete("/api/v1/keys/{id}", s.handleDeleteAPIKey)
|
||
|
||
// Admin Panel Routes
|
||
r.Group(func(r chi.Router) {
|
||
r.Use(s.adminMiddleware)
|
||
r.Get("/api/v1/admin/users", s.handleAdminGetUsers)
|
||
r.Post("/api/v1/admin/users", s.handleAdminCreateUser)
|
||
r.Patch("/api/v1/admin/users/{id}", s.handleAdminUpdateUser)
|
||
r.Put("/api/v1/admin/users/{id}/password", s.handleAdminResetPassword)
|
||
r.Delete("/api/v1/admin/users/{id}", s.handleAdminDeleteUser)
|
||
r.Get("/api/v1/admin/settings", s.handleAdminGetSettings)
|
||
r.Put("/api/v1/admin/settings", s.handleAdminUpdateSettings)
|
||
})
|
||
})
|
||
|
||
// Embedded Frontend SPA Handler
|
||
if s.staticFS != nil {
|
||
fileServer := http.FileServer(http.FS(s.staticFS))
|
||
r.Get("/*", func(w http.ResponseWriter, req *http.Request) {
|
||
path := strings.TrimPrefix(req.URL.Path, "/")
|
||
if path == "" {
|
||
path = "index.html"
|
||
}
|
||
if _, err := fs.Stat(s.staticFS, path); err != nil {
|
||
// Never return index.html for missing JS/CSS assets
|
||
if strings.HasPrefix(path, "assets/") || strings.HasSuffix(path, ".js") || strings.HasSuffix(path, ".css") {
|
||
http.NotFound(w, req)
|
||
return
|
||
}
|
||
// SPA client-side routing fallback to index.html
|
||
req.URL.Path = "/"
|
||
path = "index.html"
|
||
}
|
||
if path == "index.html" || path == "sw.js" || path == "manifest.json" {
|
||
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate")
|
||
w.Header().Set("Pragma", "no-cache")
|
||
w.Header().Set("Expires", "0")
|
||
}
|
||
fileServer.ServeHTTP(w, req)
|
||
})
|
||
}
|
||
|
||
return r
|
||
}
|
||
|
||
// --- HELPERS & MIDDLEWARE ---
|
||
|
||
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
|
||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate")
|
||
w.WriteHeader(status)
|
||
_ = json.NewEncoder(w).Encode(data)
|
||
}
|
||
|
||
func writeError(w http.ResponseWriter, status int, message string) {
|
||
writeJSON(w, status, map[string]string{"error": message})
|
||
}
|
||
|
||
func (s *Server) authMiddleware(next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
// 1. Check Authorization Bearer Token
|
||
authHeader := r.Header.Get("Authorization")
|
||
if strings.HasPrefix(authHeader, "Bearer ") {
|
||
tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
|
||
if strings.HasPrefix(tokenStr, "mindsift_sec_") {
|
||
user, err := s.store.ValidateAPIKeyAndGetUser(tokenStr)
|
||
if err == nil && user != nil {
|
||
next.ServeHTTP(w, r.WithContext(auth.WithUserID(r.Context(), user.ID)))
|
||
return
|
||
}
|
||
}
|
||
claims, err := auth.ValidateToken(s.cfg.JWTSecret, tokenStr)
|
||
if err == nil && claims != nil {
|
||
userID := claims.UserID
|
||
if _, userErr := s.store.GetUserByID(userID); userErr != nil {
|
||
if defUser, defErr := s.store.EnsureDefaultUser(); defErr == nil && defUser != nil {
|
||
userID = defUser.ID
|
||
}
|
||
}
|
||
next.ServeHTTP(w, r.WithContext(auth.WithUserID(r.Context(), userID)))
|
||
return
|
||
}
|
||
}
|
||
|
||
// 2. Check X-API-Key Header
|
||
apiKey := r.Header.Get("X-API-Key")
|
||
if apiKey != "" {
|
||
user, err := s.store.ValidateAPIKeyAndGetUser(apiKey)
|
||
if err == nil && user != nil {
|
||
next.ServeHTTP(w, r.WithContext(auth.WithUserID(r.Context(), user.ID)))
|
||
return
|
||
}
|
||
}
|
||
|
||
writeError(w, http.StatusUnauthorized, "Nicht autorisiert – bitte einloggen")
|
||
})
|
||
}
|
||
|
||
func (s *Server) adminMiddleware(next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
if userID == "" {
|
||
writeError(w, http.StatusUnauthorized, "Nicht autorisiert")
|
||
return
|
||
}
|
||
user, err := s.store.GetUserByID(userID)
|
||
if err != nil || user == nil || user.Role != "admin" {
|
||
writeError(w, http.StatusForbidden, "Administrator-Rechte erforderlich")
|
||
return
|
||
}
|
||
next.ServeHTTP(w, r)
|
||
})
|
||
}
|
||
|
||
// --- HANDLERS ---
|
||
|
||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||
"status": "ok",
|
||
"service": "mindsift",
|
||
"time": domain.NowISO(),
|
||
})
|
||
}
|
||
|
||
func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
|
||
sysSettings, _ := s.store.GetSystemSettings()
|
||
if !s.cfg.AllowRegistration || (sysSettings != nil && !sysSettings.AllowRegistration) {
|
||
count, _ := s.store.CountUsers()
|
||
if count > 0 {
|
||
writeError(w, http.StatusForbidden, "Registrierung ist auf dieser Instanz deaktiviert")
|
||
return
|
||
}
|
||
}
|
||
|
||
var req struct {
|
||
Email string `json:"email"`
|
||
Password string `json:"password"`
|
||
DisplayName string `json:"displayName"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeError(w, http.StatusBadRequest, "Ungültige Anfrage")
|
||
return
|
||
}
|
||
if strings.TrimSpace(req.Email) == "" || len(req.Password) < 4 {
|
||
writeError(w, http.StatusBadRequest, "E-Mail und Passwort (mind. 4 Zeichen) erforderlich")
|
||
return
|
||
}
|
||
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, "Fehler beim Passwort-Hashing")
|
||
return
|
||
}
|
||
|
||
count, _ := s.store.CountUsers()
|
||
role := "member"
|
||
if count == 0 {
|
||
role = "admin"
|
||
}
|
||
|
||
user, err := s.store.CreateUser(req.Email, hash, req.DisplayName, role)
|
||
if err != nil {
|
||
writeError(w, http.StatusConflict, "E-Mail-Adresse existiert bereits")
|
||
return
|
||
}
|
||
|
||
_ = s.store.SeedStarterContent(user)
|
||
|
||
token, err := auth.GenerateToken(s.cfg.JWTSecret, user.ID, user.Email, user.DisplayName)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, "Token konnte nicht erstellt werden")
|
||
return
|
||
}
|
||
|
||
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||
"token": token,
|
||
"user": user,
|
||
})
|
||
}
|
||
|
||
func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||
var req struct {
|
||
Email string `json:"email"`
|
||
Password string `json:"password"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeError(w, http.StatusBadRequest, "Ungültige Anfrage")
|
||
return
|
||
}
|
||
|
||
user, err := s.store.GetUserByEmail(req.Email)
|
||
if err != nil || !auth.CheckPassword(req.Password, user.PasswordHash) {
|
||
writeError(w, http.StatusUnauthorized, "E-Mail oder Passwort ungültig")
|
||
return
|
||
}
|
||
if !user.IsActive {
|
||
writeError(w, http.StatusForbidden, "Dieses Benutzerkonto ist deaktiviert")
|
||
return
|
||
}
|
||
|
||
token, err := auth.GenerateToken(s.cfg.JWTSecret, user.ID, user.Email, user.DisplayName)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, "Token konnte nicht erstellt werden")
|
||
return
|
||
}
|
||
|
||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||
"token": token,
|
||
"user": user,
|
||
})
|
||
}
|
||
|
||
func (s *Server) handleQuickStart(w http.ResponseWriter, r *http.Request) {
|
||
user, err := s.store.EnsureDefaultUser()
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, "Standard-Profil konnte nicht geladen werden")
|
||
return
|
||
}
|
||
|
||
token, err := auth.GenerateToken(s.cfg.JWTSecret, user.ID, user.Email, user.DisplayName)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, "Token-Fehler")
|
||
return
|
||
}
|
||
|
||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||
"token": token,
|
||
"user": user,
|
||
})
|
||
}
|
||
|
||
func (s *Server) handleMe(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
user, err := s.store.GetUserByID(userID)
|
||
if err != nil {
|
||
writeError(w, http.StatusNotFound, "Benutzer nicht gefunden")
|
||
return
|
||
}
|
||
stats, _ := s.store.GetDashboardStats(userID)
|
||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||
"user": user,
|
||
"stats": stats,
|
||
})
|
||
}
|
||
|
||
func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
stats, err := s.store.GetDashboardStats(userID)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, stats)
|
||
}
|
||
|
||
func (s *Server) handleCaptureInbox(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
var req struct {
|
||
Content string `json:"content"`
|
||
Priority string `json:"priority"`
|
||
Source string `json:"source"`
|
||
DueDate string `json:"dueDate"`
|
||
DueDate2 string `json:"due_date"`
|
||
Target string `json:"target"` // inbox | task | list_item | document
|
||
TargetListID string `json:"targetListId"`
|
||
TargetList2 string `json:"target_list_id"`
|
||
TaskListID string `json:"taskListId"`
|
||
TaskListID2 string `json:"task_list_id"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||
return
|
||
}
|
||
dueDate := req.DueDate
|
||
if dueDate == "" {
|
||
dueDate = req.DueDate2
|
||
}
|
||
targetListID := req.TargetListID
|
||
if targetListID == "" {
|
||
targetListID = req.TargetList2
|
||
}
|
||
taskListID := req.TaskListID
|
||
if taskListID == "" {
|
||
taskListID = req.TaskListID2
|
||
}
|
||
|
||
thought, err := s.store.CreateThoughtWithDetails(userID, req.Content, req.Priority, req.Source, dueDate, taskListID, "todo", "")
|
||
if err != nil {
|
||
writeError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
|
||
target := strings.TrimSpace(req.Target)
|
||
if target == "task" || target == "document" || (target == "list_item" && targetListID != "") {
|
||
if converted, convErr := s.store.UpdateThoughtTriage(userID, thought.ID, "keep", target, targetListID, dueDate, taskListID); convErr == nil && converted != nil {
|
||
thought = converted
|
||
if target == "list_item" && targetListID != "" {
|
||
if lst, lErr := s.store.GetListByID(targetListID); lErr == nil && lst != nil {
|
||
s.wsHub.Broadcast(lst.SpaceID, lst.ID, "ITEM_CREATED", lst)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
writeJSON(w, http.StatusCreated, thought)
|
||
}
|
||
|
||
func (s *Server) handleGetThoughts(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
status := r.URL.Query().Get("status")
|
||
thoughts, err := s.store.GetThoughts(userID, status, 250)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, thoughts)
|
||
}
|
||
|
||
func (s *Server) handleGetTriageDeck(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
thoughts, err := s.store.GetThoughts(userID, "inbox", 100)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, thoughts)
|
||
}
|
||
|
||
func (s *Server) handleTriageAction(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
thoughtID := chi.URLParam(r, "id")
|
||
|
||
var req struct {
|
||
Action string `json:"action"` // keep | discard | restore
|
||
ConvertTo string `json:"convertTo"`
|
||
ConvertTo2 string `json:"convert_to"`
|
||
TargetListID string `json:"targetListId"`
|
||
TargetList2 string `json:"target_list_id"`
|
||
TaskListID string `json:"taskListId"`
|
||
TaskListID2 string `json:"task_list_id"`
|
||
DueDate string `json:"dueDate"`
|
||
DueDate2 string `json:"due_date"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||
return
|
||
}
|
||
|
||
convertTo := req.ConvertTo
|
||
if convertTo == "" {
|
||
convertTo = req.ConvertTo2
|
||
}
|
||
targetListID := req.TargetListID
|
||
if targetListID == "" {
|
||
targetListID = req.TargetList2
|
||
}
|
||
taskListID := req.TaskListID
|
||
if taskListID == "" {
|
||
taskListID = req.TaskListID2
|
||
}
|
||
dueDate := req.DueDate
|
||
if dueDate == "" {
|
||
dueDate = req.DueDate2
|
||
}
|
||
|
||
updated, err := s.store.UpdateThoughtTriage(userID, thoughtID, req.Action, convertTo, targetListID, dueDate, taskListID)
|
||
if err != nil {
|
||
writeError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
|
||
// If converted to a list item, broadcast via WebSocket to the space
|
||
if convertTo == "list_item" && targetListID != "" {
|
||
if lst, err := s.store.GetListByID(targetListID); err == nil && lst != nil {
|
||
s.wsHub.Broadcast(lst.SpaceID, lst.ID, "ITEM_CREATED", lst)
|
||
}
|
||
}
|
||
|
||
writeJSON(w, http.StatusOK, updated)
|
||
}
|
||
|
||
func (s *Server) handleGetTasks(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
taskListID := r.URL.Query().Get("taskListId")
|
||
tasks, err := s.store.GetTasks(userID, taskListID)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, tasks)
|
||
}
|
||
|
||
func (s *Server) handleUpdateThought(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
thoughtID := chi.URLParam(r, "id")
|
||
|
||
var req struct {
|
||
Content *string `json:"content"`
|
||
IsCompleted *bool `json:"isCompleted"`
|
||
DueDate *string `json:"dueDate"`
|
||
Priority *string `json:"priority"`
|
||
TaskListID *string `json:"taskListId"`
|
||
KanbanStatus *string `json:"kanbanStatus"`
|
||
Notes *string `json:"notes"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||
return
|
||
}
|
||
|
||
updated, err := s.store.UpdateThoughtDetails(userID, thoughtID, req.Content, req.IsCompleted, req.DueDate, req.Priority, req.TaskListID, req.KanbanStatus, req.Notes)
|
||
if err != nil {
|
||
writeError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, updated)
|
||
}
|
||
|
||
func (s *Server) handleDeleteThought(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
thoughtID := chi.URLParam(r, "id")
|
||
if err := s.store.DeleteThought(userID, thoughtID); err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
|
||
}
|
||
|
||
func (s *Server) handlePurgeDiscarded(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
count, err := s.store.PurgeDiscardedThoughts(userID)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, map[string]int64{"purged": count})
|
||
}
|
||
|
||
// --- DOCUMENTS ---
|
||
|
||
func (s *Server) handleGetDocuments(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
docs, err := s.store.GetDocuments(userID)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, docs)
|
||
}
|
||
|
||
func (s *Server) handleCreateDocument(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
var req struct {
|
||
SpaceID string `json:"spaceId"`
|
||
Title string `json:"title"`
|
||
Icon string `json:"icon"`
|
||
FolderPath string `json:"folderPath"`
|
||
Blocks []domain.DocumentBlock `json:"blocks"`
|
||
Tags []string `json:"tags"`
|
||
IsPinned bool `json:"isPinned"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||
return
|
||
}
|
||
|
||
doc, err := s.store.CreateDocument(userID, req.SpaceID, req.Title, req.Icon, req.FolderPath, req.Blocks, req.Tags, req.IsPinned)
|
||
if err != nil {
|
||
writeError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusCreated, doc)
|
||
}
|
||
|
||
func (s *Server) handleGetDocument(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
docID := chi.URLParam(r, "id")
|
||
doc, err := s.store.GetDocumentByID(userID, docID)
|
||
if err != nil {
|
||
writeError(w, http.StatusNotFound, "Dokument nicht gefunden")
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, doc)
|
||
}
|
||
|
||
func (s *Server) handleUpdateDocument(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
docID := chi.URLParam(r, "id")
|
||
var req struct {
|
||
Title *string `json:"title"`
|
||
Icon *string `json:"icon"`
|
||
FolderPath *string `json:"folderPath"`
|
||
Blocks *[]domain.DocumentBlock `json:"blocks"`
|
||
Tags *[]string `json:"tags"`
|
||
IsPinned *bool `json:"isPinned"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||
return
|
||
}
|
||
|
||
doc, err := s.store.UpdateDocument(userID, docID, req.Title, req.Icon, req.FolderPath, req.Blocks, req.Tags, req.IsPinned)
|
||
if err != nil {
|
||
writeError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, doc)
|
||
}
|
||
|
||
func (s *Server) handleDeleteDocument(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
docID := chi.URLParam(r, "id")
|
||
if err := s.store.DeleteDocument(userID, docID); err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
|
||
}
|
||
|
||
// --- SPACES & REALTIME LISTS ---
|
||
|
||
func (s *Server) handleGetSpaces(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
spaces, err := s.store.GetSpacesForUser(userID)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, spaces)
|
||
}
|
||
|
||
func (s *Server) handleCreateSpace(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
var req struct {
|
||
Name string `json:"name"`
|
||
Icon string `json:"icon"`
|
||
Description string `json:"description"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||
return
|
||
}
|
||
|
||
sp, err := s.store.CreateSpace(userID, req.Name, req.Icon, req.Description, false)
|
||
if err != nil {
|
||
writeError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusCreated, sp)
|
||
}
|
||
|
||
func (s *Server) handleJoinSpace(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
var req struct {
|
||
InviteCode string `json:"inviteCode"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||
return
|
||
}
|
||
|
||
sp, err := s.store.JoinSpaceByInviteCode(userID, req.InviteCode)
|
||
if err != nil {
|
||
writeError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, sp)
|
||
}
|
||
|
||
func (s *Server) handleGetSpaceLists(w http.ResponseWriter, r *http.Request) {
|
||
spaceID := chi.URLParam(r, "id")
|
||
lists, err := s.store.GetListsForSpace(spaceID)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, lists)
|
||
}
|
||
|
||
func (s *Server) handleGetAllLists(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
lists, err := s.store.GetAllListsForUser(userID)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, lists)
|
||
}
|
||
|
||
func (s *Server) handleCreateList(w http.ResponseWriter, r *http.Request) {
|
||
spaceID := chi.URLParam(r, "id")
|
||
var req struct {
|
||
Title string `json:"title"`
|
||
Icon string `json:"icon"`
|
||
ListType string `json:"listType"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||
return
|
||
}
|
||
|
||
lst, err := s.store.CreateList(spaceID, req.Title, req.Icon, req.ListType)
|
||
if err != nil {
|
||
writeError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
s.wsHub.Broadcast(spaceID, lst.ID, "LIST_CREATED", lst)
|
||
writeJSON(w, http.StatusCreated, lst)
|
||
}
|
||
|
||
func (s *Server) handleDeleteList(w http.ResponseWriter, r *http.Request) {
|
||
listID := chi.URLParam(r, "id")
|
||
lst, _ := s.store.GetListByID(listID)
|
||
if err := s.store.DeleteList(listID); err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
if lst != nil {
|
||
s.wsHub.Broadcast(lst.SpaceID, listID, "LIST_DELETED", map[string]string{"listId": listID})
|
||
}
|
||
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
|
||
}
|
||
|
||
func (s *Server) handleCreateListItem(w http.ResponseWriter, r *http.Request) {
|
||
listID := chi.URLParam(r, "id")
|
||
var req struct {
|
||
Content string `json:"content"`
|
||
Category string `json:"category"`
|
||
Quantity string `json:"quantity"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||
return
|
||
}
|
||
|
||
item, err := s.store.CreateListItem(listID, req.Content, req.Category, req.Quantity, 0)
|
||
if err != nil {
|
||
writeError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
|
||
if lst, err := s.store.GetListByID(listID); err == nil && lst != nil {
|
||
s.wsHub.Broadcast(lst.SpaceID, listID, "ITEM_CREATED", item)
|
||
}
|
||
writeJSON(w, http.StatusCreated, item)
|
||
}
|
||
|
||
func (s *Server) handleToggleListItem(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
listID := chi.URLParam(r, "id")
|
||
itemID := chi.URLParam(r, "itemId")
|
||
|
||
var req struct {
|
||
IsCompleted bool `json:"isCompleted"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||
return
|
||
}
|
||
|
||
user, _ := s.store.GetUserByID(userID)
|
||
userName := "Familie"
|
||
if user != nil {
|
||
userName = user.DisplayName
|
||
}
|
||
|
||
item, err := s.store.ToggleListItem(itemID, req.IsCompleted, userID, userName)
|
||
if err != nil {
|
||
writeError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
|
||
if lst, err := s.store.GetListByID(listID); err == nil && lst != nil {
|
||
s.wsHub.Broadcast(lst.SpaceID, listID, "ITEM_TOGGLED", item)
|
||
}
|
||
writeJSON(w, http.StatusOK, item)
|
||
}
|
||
|
||
func (s *Server) handleDeleteListItem(w http.ResponseWriter, r *http.Request) {
|
||
listID := chi.URLParam(r, "id")
|
||
itemID := chi.URLParam(r, "itemId")
|
||
|
||
_, err := s.store.DeleteListItem(itemID)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
|
||
if lst, err := s.store.GetListByID(listID); err == nil && lst != nil {
|
||
s.wsHub.Broadcast(lst.SpaceID, listID, "ITEM_DELETED", map[string]string{
|
||
"itemId": itemID,
|
||
"listId": listID,
|
||
})
|
||
}
|
||
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
|
||
}
|
||
|
||
func (s *Server) handleClearCompletedItems(w http.ResponseWriter, r *http.Request) {
|
||
listID := chi.URLParam(r, "id")
|
||
removed, err := s.store.ClearCompletedListItems(listID)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
|
||
if lst, err := s.store.GetListByID(listID); err == nil && lst != nil {
|
||
s.wsHub.Broadcast(lst.SpaceID, listID, "LIST_CLEARED", lst)
|
||
}
|
||
writeJSON(w, http.StatusOK, map[string]int64{"removed": removed})
|
||
}
|
||
|
||
func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||
spaceID := chi.URLParam(r, "spaceId")
|
||
token := r.URL.Query().Get("token")
|
||
userID := "anonymous"
|
||
if token != "" {
|
||
if claims, err := auth.ValidateToken(s.cfg.JWTSecret, token); err == nil {
|
||
userID = claims.UserID
|
||
}
|
||
}
|
||
s.wsHub.ServeWS(w, r, spaceID, userID)
|
||
}
|
||
|
||
// --- API KEYS & WEBHOOK INGESTION ---
|
||
|
||
func (s *Server) handleGetAPIKeys(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
keys, err := s.store.GetAPIKeys(userID)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, keys)
|
||
}
|
||
|
||
func (s *Server) handleCreateAPIKey(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
var req struct {
|
||
Name string `json:"name"`
|
||
}
|
||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||
|
||
key, err := s.store.CreateAPIKey(userID, req.Name)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusCreated, key)
|
||
}
|
||
|
||
func (s *Server) handleDeleteAPIKey(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
keyID := chi.URLParam(r, "id")
|
||
if err := s.store.DeleteAPIKey(userID, keyID); err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
|
||
}
|
||
|
||
// handleWebhookIngest accepts Smarthome / External automation webhooks via ?token=... or X-API-Key or Bearer token
|
||
func (s *Server) handleWebhookIngest(w http.ResponseWriter, r *http.Request) {
|
||
token := r.URL.Query().Get("token")
|
||
if token == "" {
|
||
token = r.Header.Get("X-API-Key")
|
||
}
|
||
if token == "" && strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
|
||
token = strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
||
}
|
||
|
||
var user *domain.User
|
||
var err error
|
||
|
||
if strings.HasPrefix(token, "mindsift_sec_") {
|
||
user, err = s.store.ValidateAPIKeyAndGetUser(token)
|
||
} else if token != "" {
|
||
if claims, jwtErr := auth.ValidateToken(s.cfg.JWTSecret, token); jwtErr == nil {
|
||
user, err = s.store.GetUserByID(claims.UserID)
|
||
}
|
||
}
|
||
|
||
if err != nil || user == nil {
|
||
writeError(w, http.StatusUnauthorized, "Ungültiger Webhook-Token oder API-Key")
|
||
return
|
||
}
|
||
|
||
var req struct {
|
||
Title string `json:"title"`
|
||
Content string `json:"content"`
|
||
Priority string `json:"priority"`
|
||
Tags []string `json:"tags"`
|
||
DueDate string `json:"dueDate"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeError(w, http.StatusBadRequest, "Ungültiges JSON-Payload")
|
||
return
|
||
}
|
||
|
||
bodyText := strings.TrimSpace(req.Title)
|
||
if req.Content != "" {
|
||
if bodyText != "" {
|
||
bodyText = bodyText + " — " + strings.TrimSpace(req.Content)
|
||
} else {
|
||
bodyText = strings.TrimSpace(req.Content)
|
||
}
|
||
}
|
||
if bodyText == "" {
|
||
writeError(w, http.StatusBadRequest, "Feld 'title' oder 'content' ist erforderlich")
|
||
return
|
||
}
|
||
|
||
for _, tag := range req.Tags {
|
||
cleanTag := strings.TrimPrefix(strings.TrimSpace(tag), "#")
|
||
if cleanTag != "" && !strings.Contains(strings.ToLower(bodyText), "#"+strings.ToLower(cleanTag)) {
|
||
bodyText += fmt.Sprintf(" #%s", cleanTag)
|
||
}
|
||
}
|
||
|
||
if req.Priority == "" {
|
||
req.Priority = "high"
|
||
}
|
||
|
||
thought, err := s.store.CreateThought(user.ID, bodyText, req.Priority, "webhook", req.DueDate)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
|
||
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||
"status": "ingested",
|
||
"thought": thought,
|
||
})
|
||
}
|
||
|
||
// --- SEARCH & EXPORT ---
|
||
|
||
func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
q := r.URL.Query().Get("q")
|
||
thoughts, docs, err := s.store.SearchAll(userID, q)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||
"thoughts": thoughts,
|
||
"documents": docs,
|
||
})
|
||
}
|
||
|
||
func (s *Server) handleExport(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
exportData, err := s.store.ExportUserData(userID)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
w.Header().Set("Content-Disposition", "attachment; filename=mindsift-export.json")
|
||
writeJSON(w, http.StatusOK, exportData)
|
||
}
|
||
|
||
// --- USER SETTINGS ---
|
||
|
||
func (s *Server) handleUpdateUserSettings(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
var req struct {
|
||
DisplayName *string `json:"displayName"`
|
||
Email *string `json:"email"`
|
||
Password *string `json:"password"`
|
||
DiscordWebhookURL *string `json:"discordWebhookUrl"`
|
||
NotifyBeforeMinutes *int `json:"notifyBeforeMinutes"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||
return
|
||
}
|
||
if req.Password != nil && strings.TrimSpace(*req.Password) != "" {
|
||
hash, err := auth.HashPassword(*req.Password)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, "Fehler beim Hashen des Passworts")
|
||
return
|
||
}
|
||
_ = s.store.UpdateUserPassword(userID, hash)
|
||
}
|
||
u, err := s.store.UpdateUser(userID, req.DisplayName, req.Email, nil, nil, req.DiscordWebhookURL, req.NotifyBeforeMinutes)
|
||
if err != nil {
|
||
writeError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, u)
|
||
}
|
||
|
||
// --- TASK LISTS ---
|
||
|
||
func (s *Server) handleGetTaskLists(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
lists, err := s.store.GetTaskLists(userID)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, lists)
|
||
}
|
||
|
||
func (s *Server) handleCreateTaskList(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
var req struct {
|
||
SpaceID string `json:"spaceId"`
|
||
Title string `json:"title"`
|
||
Icon string `json:"icon"`
|
||
Color string `json:"color"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||
return
|
||
}
|
||
tl, err := s.store.CreateTaskList(userID, req.SpaceID, req.Title, req.Icon, req.Color)
|
||
if err != nil {
|
||
writeError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusCreated, tl)
|
||
}
|
||
|
||
func (s *Server) handleUpdateTaskList(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
listID := chi.URLParam(r, "id")
|
||
var req struct {
|
||
Title *string `json:"title"`
|
||
Icon *string `json:"icon"`
|
||
Color *string `json:"color"`
|
||
SpaceID *string `json:"spaceId"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||
return
|
||
}
|
||
tl, err := s.store.UpdateTaskList(userID, listID, req.Title, req.Icon, req.Color, req.SpaceID)
|
||
if err != nil {
|
||
writeError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, tl)
|
||
}
|
||
|
||
func (s *Server) handleDeleteTaskList(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
listID := chi.URLParam(r, "id")
|
||
if err := s.store.DeleteTaskList(userID, listID); err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
|
||
}
|
||
|
||
// --- ORGANIZATIONS & TEAMS ---
|
||
|
||
func (s *Server) handleGetOrganizations(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
orgs, err := s.store.GetOrganizationsForUser(userID)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, orgs)
|
||
}
|
||
|
||
func (s *Server) handleCreateOrganization(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
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(userID, req.Name)
|
||
if err != nil {
|
||
writeError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusCreated, org)
|
||
}
|
||
|
||
func (s *Server) handleGetOrgTeams(w http.ResponseWriter, r *http.Request) {
|
||
orgID := chi.URLParam(r, "id")
|
||
teams, err := s.store.GetTeamsForOrg(orgID)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, teams)
|
||
}
|
||
|
||
func (s *Server) handleCreateOrgTeam(w http.ResponseWriter, r *http.Request) {
|
||
orgID := chi.URLParam(r, "id")
|
||
var req struct {
|
||
Name string `json:"name"`
|
||
Description string `json:"description"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||
return
|
||
}
|
||
team, err := s.store.CreateTeam(orgID, req.Name, req.Description)
|
||
if err != nil {
|
||
writeError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusCreated, team)
|
||
}
|
||
|
||
func (s *Server) handleGetUserTeams(w http.ResponseWriter, r *http.Request) {
|
||
userID := auth.GetUserID(r.Context())
|
||
teams, err := s.store.GetTeamsForUser(userID)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, teams)
|
||
}
|
||
|
||
func (s *Server) handleAddTeamMember(w http.ResponseWriter, r *http.Request) {
|
||
teamID := chi.URLParam(r, "id")
|
||
var req struct {
|
||
UserID string `json:"userId"`
|
||
Role string `json:"role"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||
return
|
||
}
|
||
if err := s.store.AddTeamMember(teamID, req.UserID, req.Role); err != nil {
|
||
writeError(w, http.StatusBadRequest, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, map[string]bool{"added": true})
|
||
}
|
||
|
||
func (s *Server) handleRemoveTeamMember(w http.ResponseWriter, r *http.Request) {
|
||
teamID := chi.URLParam(r, "id")
|
||
memberUserID := chi.URLParam(r, "userId")
|
||
if err := s.store.RemoveTeamMember(teamID, memberUserID); err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, map[string]bool{"removed": true})
|
||
}
|
||
|
||
// --- ADMIN 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) {
|
||
var req struct {
|
||
Email string `json:"email"`
|
||
Password string `json:"password"`
|
||
DisplayName string `json:"displayName"`
|
||
Role string `json:"role"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||
return
|
||
}
|
||
if req.Email == "" || req.Password == "" {
|
||
writeError(w, http.StatusBadRequest, "E-Mail und Passwort erforderlich")
|
||
return
|
||
}
|
||
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
|
||
}
|
||
writeJSON(w, http.StatusCreated, u)
|
||
}
|
||
|
||
func (s *Server) handleAdminUpdateUser(w http.ResponseWriter, r *http.Request) {
|
||
targetUserID := chi.URLParam(r, "id")
|
||
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
|
||
}
|
||
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
|
||
}
|
||
writeJSON(w, http.StatusOK, u)
|
||
}
|
||
|
||
func (s *Server) handleAdminResetPassword(w http.ResponseWriter, r *http.Request) {
|
||
targetUserID := chi.URLParam(r, "id")
|
||
var req struct {
|
||
Password string `json:"password"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.Password) == "" {
|
||
writeError(w, http.StatusBadRequest, "Neues Passwort erforderlich")
|
||
return
|
||
}
|
||
hash, err := auth.HashPassword(req.Password)
|
||
if err != nil {
|
||
writeError(w, http.StatusInternalServerError, "Passwort-Hash fehlgeschlagen")
|
||
return
|
||
}
|
||
if err := s.store.UpdateUserPassword(targetUserID, hash); err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
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
|
||
}
|
||
if err := s.store.DeleteUser(targetUserID); err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
|
||
}
|
||
|
||
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
|
||
}
|
||
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
|
||
}
|
||
if err := s.store.UpdateSystemSettings(&settings); err != nil {
|
||
writeError(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, http.StatusOK, settings)
|
||
}
|
||
|
||
// --- DISCORD ALARMS BACKGROUND SCHEDULER ---
|
||
|
||
func (s *Server) startDiscordAlarmTicker() {
|
||
ticker := time.NewTicker(60 * time.Second)
|
||
defer ticker.Stop()
|
||
|
||
httpClient := &http.Client{Timeout: 10 * time.Second}
|
||
|
||
for range ticker.C {
|
||
alarms, err := s.store.GetDueTasksForAlarm()
|
||
if err != nil || len(alarms) == 0 {
|
||
continue
|
||
}
|
||
|
||
today := time.Now().Format("2006-01-02")
|
||
|
||
for _, a := range alarms {
|
||
if a.DueDate <= today && a.DiscordWebhookURL != "" {
|
||
payload := map[string]interface{}{
|
||
"content": fmt.Sprintf("⏰ **MindSift Aufgaben-Erinnerung**"),
|
||
"embeds": []map[string]interface{}{
|
||
{
|
||
"title": a.Title,
|
||
"description": fmt.Sprintf("Diese Aufgabe ist fällig am **%s** (Priorität: %s)", a.DueDate, a.Priority),
|
||
"color": 3066993,
|
||
"footer": map[string]string{
|
||
"text": "MindSift Reminder",
|
||
},
|
||
},
|
||
},
|
||
}
|
||
body, mErr := json.Marshal(payload)
|
||
if mErr == nil {
|
||
resp, pErr := httpClient.Post(a.DiscordWebhookURL, "application/json", bytes.NewReader(body))
|
||
if pErr == nil {
|
||
_ = resp.Body.Close()
|
||
_ = s.store.MarkTaskAlarmSent(a.TaskID)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|