Files
MindSift/internal/api/handlers_account.go
T
sandrodachandClaude Opus 5.5 74c394efaa
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m31s
refactor: split sqlite.go and server.go by domain (FLAW-20)
Pure moves, no behaviour change: the storage layer now has users,
apikeys, thoughts, tasklists, documents, folders, tags, lists, teams and
system files next to sqlite.go (store, schema, migrations); the API has
handlers_*.go per area plus realtime.go and alarms.go next to server.go
(server, router, middleware). Declarations were moved verbatim with an
AST-based tool; imports pruned with goimports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 08:14:33 +01:00

159 lines
5.6 KiB
Go

package api
import (
"bytes"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"strings"
"time"
"github.com/go-chi/chi/v5"
"github.com/sandrodach/mindsift/internal/auth"
"github.com/sandrodach/mindsift/internal/domain"
"github.com/sandrodach/mindsift/internal/storage"
)
// --- CUSTOM THEMES ---
func (s *Server) handleGetThemes(w http.ResponseWriter, r *http.Request) {
themes, err := s.store.GetUserThemes(auth.GetUserID(r.Context()))
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, themes)
}
// handleSaveTheme creates a theme, or updates it when an id of the caller's theme is given.
func (s *Server) handleSaveTheme(w http.ResponseWriter, r *http.Request) {
var req struct {
ID string `json:"id"`
Name string `json:"name"`
CSS string `json:"css"`
}
r.Body = http.MaxBytesReader(w, r.Body, storage.MaxThemeCSSBytes+16*1024)
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungültige Anfrage oder Theme zu groß")
return
}
theme, err := s.store.SaveUserTheme(auth.GetUserID(r.Context()), strings.TrimSpace(req.ID), req.Name, req.CSS)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
status := http.StatusCreated
if req.ID != "" {
status = http.StatusOK
}
writeJSON(w, status, theme)
}
func (s *Server) handleDeleteTheme(w http.ResponseWriter, r *http.Request) {
if err := s.store.DeleteUserTheme(auth.GetUserID(r.Context()), chi.URLParam(r, "id")); err != nil {
writeError(w, http.StatusNotFound, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
}
// confirmPassword checks the current password sent as {"password": "…"} before destructive actions.
func (s *Server) confirmPassword(w http.ResponseWriter, r *http.Request) (*domain.User, bool) {
var req struct {
Password string `json:"password"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
user, err := s.store.GetUserByID(auth.GetUserID(r.Context()))
if err != nil || user == nil {
writeError(w, http.StatusUnauthorized, "Nicht autorisiert")
return nil, false
}
if req.Password == "" || !auth.CheckPassword(req.Password, user.PasswordHash) {
writeError(w, http.StatusForbidden, "Passwort ist falsch")
return nil, false
}
return user, true
}
// runImport answers a preview (?preview=1) with what would be created, otherwise imports.
func (s *Server) runImport(w http.ResponseWriter, r *http.Request, plan *storage.ImportPlan, err error) {
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
if r.URL.Query().Get("preview") == "1" {
writeJSON(w, http.StatusOK, plan.Summary(false))
return
}
userID := auth.GetUserID(r.Context())
// mode=restore (MindSift data only): replace everything instead of adding next to it. The plan is
// fully read and validated at this point, so a broken file never wipes anything.
if r.URL.Query().Get("mode") == "restore" && strings.HasSuffix(r.URL.Path, "/mindsift") {
if err := s.store.PurgeUserData(userID); err != nil {
writeError(w, http.StatusInternalServerError, "Wiederherstellen abgebrochen: "+err.Error())
return
}
}
sum, err := s.store.ApplyImport(userID, plan)
if err != nil {
slog.Error("Import abgebrochen", "user_id", userID, "error", err)
writeError(w, http.StatusInternalServerError, "Import abgebrochen: "+err.Error())
return
}
writeJSON(w, http.StatusOK, sum)
}
// handleImportMindSift restores MindSift's own data: the JSON backup or the Markdown archive (ZIP).
func (s *Server) handleImportMindSift(w http.ResponseWriter, r *http.Request) {
data, name, err := importUpload(w, r)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
plan, err := storage.PlanMindSiftImport(data, name)
s.runImport(w, r, plan, err)
}
// handleImportObsidian imports an Obsidian vault (ZIP) into a folder named after the vault.
func (s *Server) handleImportObsidian(w http.ResponseWriter, r *http.Request) {
data, name, err := importUpload(w, r)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
plan, err := storage.PlanObsidianImport(bytes.NewReader(data), int64(len(data)), name)
s.runImport(w, r, plan, err)
}
// handleExportMarkdown streams an Obsidian-compatible ZIP of the user's workspace (no temp files).
func (s *Server) handleExportMarkdown(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
w.Header().Set("Content-Type", "application/zip")
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="mindsift-vault-%s.zip"`, time.Now().Format("2006-01-02")))
if err := s.store.WriteMarkdownVault(userID, w); err != nil {
// Headers are already sent; the client gets a truncated archive
slog.Error("Markdown-Export fehlgeschlagen", "user_id", userID, "error", err)
}
}
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
}
// ?format=zip: the same JSON compressed (backups grow quickly with notes); plain JSON stays the API default
if r.URL.Query().Get("format") == "zip" {
w.Header().Set("Content-Type", "application/zip")
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="mindsift-backup-%s.zip"`, time.Now().Format("2006-01-02")))
if err := storage.WriteBackupZip(exportData, w); err != nil {
slog.Error("Backup-Export fehlgeschlagen", "user_id", userID, "error", err)
}
return
}
w.Header().Set("Content-Disposition", "attachment; filename=mindsift-export.json")
writeJSON(w, http.StatusOK, exportData)
}