rearranged go methods+files
authorPhiTux <redacted>
Mon, 6 Oct 2025 19:33:30 +0000 (21:33 +0200)
committerPhiTux <redacted>
Mon, 6 Oct 2025 19:33:30 +0000 (21:33 +0200)
backend/handlers/admin.go
backend/handlers/files.go [new file with mode: 0644]
backend/handlers/logs.go
backend/handlers/search.go [moved from backend/handlers/additional.go with 64% similarity]
backend/handlers/settings.go [new file with mode: 0644]
backend/handlers/tags.go [new file with mode: 0644]
backend/handlers/templates.go [new file with mode: 0644]
backend/handlers/users.go

index e62cfa3459fb5a5a84118c5f8084bd3924d565ce..897b6bade00dffb9cfafa70cd08943c1bd718af3 100644 (file)
@@ -308,8 +308,6 @@ func OpenRegistrationTemp(w http.ResponseWriter, r *http.Request) {
                return
        }
 
-       log.Printf("%v", r.Body)
-
        // Default duration 5 minutes; optionally allow custom seconds (max 15 min)
        duration := 5 * 60 // seconds
        if req.Seconds > 0 && req.Seconds <= 15*60 {
diff --git a/backend/handlers/files.go b/backend/handlers/files.go
new file mode 100644 (file)
index 0000000..95f409a
--- /dev/null
@@ -0,0 +1,647 @@
+package handlers
+
+import (
+       "encoding/json"
+       "fmt"
+       "io"
+       "net/http"
+       "strconv"
+       "strings"
+
+       "github.com/phitux/dailytxt/backend/utils"
+)
+
+// UploadFile handles uploading a file
+func UploadFile(w http.ResponseWriter, r *http.Request) {
+       // Get user ID and derived key from context
+       userID, ok := r.Context().Value(utils.UserIDKey).(int)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+       derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+
+       // Parse form
+       if err := r.ParseMultipartForm(10 << 20); err != nil { // 10 MB max
+               http.Error(w, fmt.Sprintf("Error parsing form: %v", err), http.StatusBadRequest)
+               return
+       }
+
+       // Get form values
+       dayStr := r.FormValue("day")
+       if dayStr == "" {
+               http.Error(w, "Missing day parameter", http.StatusBadRequest)
+               return
+       }
+       day, err := strconv.Atoi(dayStr)
+       if err != nil {
+               http.Error(w, "Invalid day parameter", http.StatusBadRequest)
+               return
+       }
+
+       monthStr := r.FormValue("month")
+       if monthStr == "" {
+               http.Error(w, "Missing month parameter", http.StatusBadRequest)
+               return
+       }
+       month, err := strconv.Atoi(monthStr)
+       if err != nil {
+               http.Error(w, "Invalid month parameter", http.StatusBadRequest)
+               return
+       }
+
+       yearStr := r.FormValue("year")
+       if yearStr == "" {
+               http.Error(w, "Missing year parameter", http.StatusBadRequest)
+               return
+       }
+       year, err := strconv.Atoi(yearStr)
+       if err != nil {
+               http.Error(w, "Invalid year parameter", http.StatusBadRequest)
+               return
+       }
+
+       uuid := r.FormValue("uuid")
+       if uuid == "" {
+               http.Error(w, "Missing uuid parameter", http.StatusBadRequest)
+               return
+       }
+
+       // Get file
+       file, header, err := r.FormFile("file")
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error getting file: %v", err), http.StatusBadRequest)
+               return
+       }
+       defer file.Close()
+
+       // Get encryption key first (before reading large file)
+       encKey, err := utils.GetEncryptionKey(userID, derivedKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error getting encryption key: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Read file into a buffer (more memory efficient)
+       fileBytes, err := io.ReadAll(file)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError)
+               return
+       }
+       // Ensure fileBytes is cleared when function exits
+       defer func() { fileBytes = nil }()
+
+       // Encrypt file
+       encryptedFile, err := utils.EncryptFile(fileBytes, encKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error encrypting file: %v", err), http.StatusInternalServerError)
+               return
+       }
+       // Ensure encryptedFile is cleared when function exits
+       defer func() { encryptedFile = nil }()
+
+       // Clear original file data from memory immediately after encryption
+       fileBytes = nil
+
+       // Write file
+       if err := utils.WriteFile(encryptedFile, userID, uuid); err != nil {
+               http.Error(w, fmt.Sprintf("Error writing file: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Clear encrypted data from memory immediately after writing
+       encryptedFile = nil
+
+       // Get month data
+       content, err := utils.GetMonth(userID, year, month)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error retrieving month data: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Encrypt filename
+       encFilename, err := utils.EncryptText(header.Filename, encKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error encrypting filename: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Create new file entry
+       newFile := map[string]any{
+               "enc_filename":  encFilename,
+               "uuid_filename": uuid,
+               "size":          header.Size,
+       }
+
+       // Add file to day
+       days, ok := content["days"].([]any)
+       if !ok {
+               days = []any{}
+       }
+
+       dayFound := false
+       for i, dayInterface := range days {
+               dayObj, ok := dayInterface.(map[string]any)
+               if !ok {
+                       continue
+               }
+
+               dayNum, ok := dayObj["day"].(float64)
+               if !ok || int(dayNum) != day {
+                       continue
+               }
+
+               // Add file to existing day
+               dayFound = true
+               files, ok := dayObj["files"].([]any)
+               if !ok {
+                       files = []any{}
+               }
+               files = append(files, newFile)
+               dayObj["files"] = files
+               days[i] = dayObj
+               break
+       }
+
+       if !dayFound {
+               // Create new day with file
+               days = append(days, map[string]any{
+                       "day":   day,
+                       "files": []any{newFile},
+               })
+       }
+
+       // Update days array
+       content["days"] = days
+
+       // Write month data
+       if err := utils.WriteMonth(userID, year, month, content); err != nil {
+               // Cleanup on error
+               utils.RemoveFile(userID, uuid)
+               http.Error(w, fmt.Sprintf("Error writing month data: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Return success
+       utils.JSONResponse(w, http.StatusOK, map[string]bool{
+               "success": true,
+       })
+}
+
+// DownloadFile handles downloading a file
+func DownloadFile(w http.ResponseWriter, r *http.Request) {
+       // Get user ID and derived key from context
+       userID, ok := r.Context().Value(utils.UserIDKey).(int)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+       derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+
+       // Get uuid parameter
+       uuid := r.URL.Query().Get("uuid")
+       if uuid == "" {
+               http.Error(w, "Missing uuid parameter", http.StatusBadRequest)
+               return
+       }
+
+       // Get encryption key
+       encKey, err := utils.GetEncryptionKey(userID, derivedKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error getting encryption key: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Read file
+       encryptedFile, err := utils.ReadFile(userID, uuid)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError)
+               return
+       }
+       // Ensure encryptedFile is cleared when function exits
+       defer func() { encryptedFile = nil }()
+
+       // Decrypt file
+       decryptedFile, err := utils.DecryptFile(encryptedFile, encKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error decrypting file: %v", err), http.StatusInternalServerError)
+               return
+       }
+       // Ensure decryptedFile is cleared when function exits
+       defer func() { decryptedFile = nil }()
+
+       // Clear encrypted data from memory immediately after decryption
+       encryptedFile = nil
+
+       // Set response headers for streaming
+       w.Header().Set("Content-Type", "application/octet-stream")
+       w.Header().Set("Content-Disposition", "attachment")
+
+       // Write file to response
+       if _, err := w.Write(decryptedFile); err != nil {
+               http.Error(w, fmt.Sprintf("Error writing response: %v", err), http.StatusInternalServerError)
+               return
+       }
+}
+
+// DeleteFile handles deleting a file
+func DeleteFile(w http.ResponseWriter, r *http.Request) {
+       // Get user ID from context
+       userID, ok := r.Context().Value(utils.UserIDKey).(int)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+
+       // Get parameters
+       uuid := r.URL.Query().Get("uuid")
+       if uuid == "" {
+               http.Error(w, "Missing uuid parameter", http.StatusBadRequest)
+               return
+       }
+
+       dayStr := r.URL.Query().Get("day")
+       if dayStr == "" {
+               http.Error(w, "Missing day parameter", http.StatusBadRequest)
+               return
+       }
+       day, err := strconv.Atoi(dayStr)
+       if err != nil {
+               http.Error(w, "Invalid day parameter", http.StatusBadRequest)
+               return
+       }
+
+       monthStr := r.URL.Query().Get("month")
+       if monthStr == "" {
+               http.Error(w, "Missing month parameter", http.StatusBadRequest)
+               return
+       }
+       month, err := strconv.Atoi(monthStr)
+       if err != nil {
+               http.Error(w, "Invalid month parameter", http.StatusBadRequest)
+               return
+       }
+
+       yearStr := r.URL.Query().Get("year")
+       if yearStr == "" {
+               http.Error(w, "Missing year parameter", http.StatusBadRequest)
+               return
+       }
+       year, err := strconv.Atoi(yearStr)
+       if err != nil {
+               http.Error(w, "Invalid year parameter", http.StatusBadRequest)
+               return
+       }
+
+       // Get month data
+       content, err := utils.GetMonth(userID, year, month)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error retrieving month data: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Check if days exist
+       days, ok := content["days"].([]any)
+       if !ok {
+               http.Error(w, "Day not found - json error", http.StatusInternalServerError)
+               return
+       }
+
+       // Find day and file
+       fileFound := false
+       for i, dayInterface := range days {
+               dayObj, ok := dayInterface.(map[string]any)
+               if !ok {
+                       continue
+               }
+
+               dayNum, ok := dayObj["day"].(float64)
+               if !ok || int(dayNum) != day {
+                       continue
+               }
+
+               // Check for files
+               files, ok := dayObj["files"].([]any)
+               if !ok {
+                       continue
+               }
+
+               // Find file
+               for j, fileInterface := range files {
+                       file, ok := fileInterface.(map[string]any)
+                       if !ok {
+                               continue
+                       }
+
+                       uuidFilename, ok := file["uuid_filename"].(string)
+                       if !ok || uuidFilename != uuid {
+                               continue
+                       }
+
+                       // Remove file from array
+                       if err := utils.RemoveFile(userID, uuid); err != nil {
+                               http.Error(w, fmt.Sprintf("Failed to delete file: %v", err), http.StatusInternalServerError)
+                               return
+                       }
+
+                       files = append(files[:j], files[j+1:]...)
+                       dayObj["files"] = files
+                       days[i] = dayObj
+                       fileFound = true
+                       break
+               }
+
+               if fileFound {
+                       break
+               }
+       }
+
+       if !fileFound {
+               http.Error(w, "Failed to delete file - not found in log", http.StatusInternalServerError)
+               return
+       }
+
+       // Update days array
+       content["days"] = days
+
+       // Write month data
+       if err := utils.WriteMonth(userID, year, month, content); err != nil {
+               http.Error(w, fmt.Sprintf("Failed to write changes of deleted file: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Return success
+       utils.JSONResponse(w, http.StatusOK, map[string]bool{
+               "success": true,
+       })
+}
+
+// RenameFileRequest represents the rename file request body
+type RenameFileRequest struct {
+       UUID        string `json:"uuid"`
+       NewFilename string `json:"new_filename"`
+       Day         int    `json:"day"`
+       Month       int    `json:"month"`
+       Year        int    `json:"year"`
+}
+
+// RenameFile handles renaming a file
+func RenameFile(w http.ResponseWriter, r *http.Request) {
+       // Get user ID from context
+       userID, ok := r.Context().Value(utils.UserIDKey).(int)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+       derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+
+       // Parse request body
+       var req RenameFileRequest
+       if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+               http.Error(w, "Invalid request body", http.StatusBadRequest)
+               return
+       }
+
+       // Validate input
+       req.NewFilename = strings.TrimSpace(req.NewFilename)
+       if req.NewFilename == "" {
+               utils.JSONResponse(w, http.StatusBadRequest, map[string]any{
+                       "success": false,
+                       "message": "New filename cannot be empty",
+               })
+               return
+       }
+
+       if req.UUID == "" {
+               utils.JSONResponse(w, http.StatusBadRequest, map[string]any{
+                       "success": false,
+                       "message": "File UUID is required",
+               })
+               return
+       }
+
+       // Get month data
+       content, err := utils.GetMonth(userID, req.Year, req.Month)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error retrieving month data: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       encKey, err := utils.GetEncryptionKey(userID, derivedKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error getting encryption key: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       enc_filename, err := utils.EncryptText(req.NewFilename, encKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error encrypting text: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Find and update the file
+       days, ok := content["days"].([]any)
+       if !ok {
+               utils.JSONResponse(w, http.StatusNotFound, map[string]any{
+                       "success": false,
+                       "message": "No days found",
+               })
+               return
+       }
+
+       found := false
+       for _, d := range days {
+               day, ok := d.(map[string]any)
+               if !ok {
+                       continue
+               }
+
+               dayNum, ok := day["day"].(float64)
+               if !ok || int(dayNum) != req.Day {
+                       continue
+               }
+
+               files, ok := day["files"].([]any)
+               if !ok {
+                       continue
+               }
+
+               // Find and rename the specific file
+               for _, f := range files {
+                       file, ok := f.(map[string]any)
+                       if !ok {
+                               continue
+                       }
+
+                       if uuid, ok := file["uuid_filename"].(string); ok && uuid == req.UUID {
+                               file["enc_filename"] = enc_filename
+                               found = true
+                               break
+                       }
+               }
+
+               if found {
+                       break
+               }
+       }
+
+       if !found {
+               utils.JSONResponse(w, http.StatusNotFound, map[string]any{
+                       "success": false,
+                       "message": "File not found",
+               })
+               return
+       }
+
+       // Save the updated month data
+       if err := utils.WriteMonth(userID, req.Year, req.Month, content); err != nil {
+               http.Error(w, fmt.Sprintf("Error writing month data: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       utils.Logger.Printf("File renamed successfully for user %d: %s -> %s", userID, req.UUID, req.NewFilename)
+       utils.JSONResponse(w, http.StatusOK, map[string]bool{"success": true})
+}
+
+// ReorderFilesRequest represents the reorder files request body
+type ReorderFilesRequest struct {
+       Day       int            `json:"day"`
+       Month     int            `json:"month"`
+       Year      int            `json:"year"`
+       FileOrder map[string]int `json:"file_order"` // UUID -> order index
+}
+
+// ReorderFiles handles reordering files within a day
+func ReorderFiles(w http.ResponseWriter, r *http.Request) {
+       // Get user ID from context
+       userID, ok := r.Context().Value(utils.UserIDKey).(int)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+
+       // Parse request body
+       var req ReorderFilesRequest
+       if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+               http.Error(w, "Invalid request body", http.StatusBadRequest)
+               return
+       }
+
+       if len(req.FileOrder) == 0 {
+               utils.JSONResponse(w, http.StatusBadRequest, map[string]any{
+                       "success": false,
+                       "message": "File order mapping is required",
+               })
+               return
+       }
+
+       // Get month data
+       content, err := utils.GetMonth(userID, req.Year, req.Month)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error retrieving month data: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Find and reorder files for the specific day
+       days, ok := content["days"].([]any)
+       if !ok {
+               utils.JSONResponse(w, http.StatusNotFound, map[string]any{
+                       "success": false,
+                       "message": "No days found",
+               })
+               return
+       }
+
+       found := false
+       for _, d := range days {
+               day, ok := d.(map[string]any)
+               if !ok {
+                       continue
+               }
+
+               dayNum, ok := day["day"].(float64)
+               if !ok || int(dayNum) != req.Day {
+                       continue
+               }
+
+               files, ok := day["files"].([]any)
+               if !ok {
+                       continue
+               }
+
+               // Create a slice to hold files with their new order
+               type fileWithOrder struct {
+                       file  map[string]any
+                       order int
+               }
+
+               var orderedFiles []fileWithOrder
+
+               // Assign order to each file
+               for _, f := range files {
+                       file, ok := f.(map[string]any)
+                       if !ok {
+                               continue
+                       }
+
+                       uuid, ok := file["uuid_filename"].(string)
+                       if !ok {
+                               continue
+                       }
+
+                       if order, exists := req.FileOrder[uuid]; exists {
+                               orderedFiles = append(orderedFiles, fileWithOrder{file: file, order: order})
+                       } else {
+                               // Files not in the reorder map get appended at the end
+                               orderedFiles = append(orderedFiles, fileWithOrder{file: file, order: len(req.FileOrder)})
+                       }
+               }
+
+               // Sort files by their order
+               for i := 0; i < len(orderedFiles)-1; i++ {
+                       for j := i + 1; j < len(orderedFiles); j++ {
+                               if orderedFiles[i].order > orderedFiles[j].order {
+                                       orderedFiles[i], orderedFiles[j] = orderedFiles[j], orderedFiles[i]
+                               }
+                       }
+               }
+
+               // Update the files array with the new order
+               newFiles := make([]any, len(orderedFiles))
+               for i, fileWithOrder := range orderedFiles {
+                       newFiles[i] = fileWithOrder.file
+               }
+               day["files"] = newFiles
+
+               found = true
+               break
+       }
+
+       if !found {
+               utils.JSONResponse(w, http.StatusNotFound, map[string]any{
+                       "success": false,
+                       "message": "Day not found",
+               })
+               return
+       }
+
+       // Save the updated month data
+       if err := utils.WriteMonth(userID, req.Year, req.Month, content); err != nil {
+               http.Error(w, fmt.Sprintf("Error writing month data: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       utils.JSONResponse(w, http.StatusOK, map[string]bool{"success": true})
+}
index faf59af890e0d0174499f777e1f63fada0207587..764a7cf9bbeffc3e1a95b35a39bac1d83254641e 100644 (file)
@@ -391,149 +391,109 @@ func GetMarkedDays(w http.ResponseWriter, r *http.Request) {
        })
 }
 
-// TemplatesRequest represents a templates request
-type TemplatesRequest struct {
-       Templates []struct {
-               Name string `json:"name"`
-               Text string `json:"text"`
-       } `json:"templates"`
-}
-
-// GetTemplates handles retrieving a user's templates
-func GetTemplates(w http.ResponseWriter, r *http.Request) {
-       // Get user ID and derived key from context
+// BookmarkDay handles bookmarking a day
+func BookmarkDay(w http.ResponseWriter, r *http.Request) {
+       // Get user ID from context
        userID, ok := r.Context().Value(utils.UserIDKey).(int)
        if !ok {
                http.Error(w, "Unauthorized", http.StatusUnauthorized)
                return
        }
-       derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+
+       // Get parameters
+       dayStr := r.URL.Query().Get("day")
+       if dayStr == "" {
+               http.Error(w, "Missing day parameter", http.StatusBadRequest)
                return
        }
-
-       // Get templates
-       content, err := utils.GetTemplates(userID)
+       day, err := strconv.Atoi(dayStr)
        if err != nil {
-               http.Error(w, fmt.Sprintf("Error retrieving templates: %v", err), http.StatusInternalServerError)
+               http.Error(w, "Invalid day parameter", http.StatusBadRequest)
                return
        }
 
-       // If no templates, return empty array
-       if templates, ok := content["templates"].([]any); !ok || len(templates) == 0 {
-               utils.JSONResponse(w, http.StatusOK, []any{})
+       monthStr := r.URL.Query().Get("month")
+       if monthStr == "" {
+               http.Error(w, "Missing month parameter", http.StatusBadRequest)
                return
        }
-
-       // Get encryption key
-       encKey, err := utils.GetEncryptionKey(userID, derivedKey)
+       month, err := strconv.Atoi(monthStr)
        if err != nil {
-               http.Error(w, fmt.Sprintf("Error getting encryption key: %v", err), http.StatusInternalServerError)
+               http.Error(w, "Invalid month parameter", http.StatusBadRequest)
                return
        }
 
-       // Decrypt template data
-       templates := content["templates"].([]any)
-       result := []any{}
-
-       for _, templateInterface := range templates {
-               template, ok := templateInterface.(map[string]any)
-               if !ok {
-                       continue
-               }
-
-               // Decrypt name and text
-               if encName, ok := template["name"].(string); ok {
-                       decryptedName, err := utils.DecryptText(encName, encKey)
-                       if err != nil {
-                               http.Error(w, fmt.Sprintf("Error decrypting template name: %v", err), http.StatusInternalServerError)
-                               return
-                       }
-                       template["name"] = decryptedName
-               }
-
-               if encText, ok := template["text"].(string); ok {
-                       decryptedText, err := utils.DecryptText(encText, encKey)
-                       if err != nil {
-                               http.Error(w, fmt.Sprintf("Error decrypting template text: %v", err), http.StatusInternalServerError)
-                               return
-                       }
-                       template["text"] = decryptedText
-               }
-
-               result = append(result, template)
-       }
-
-       // Return templates
-       utils.JSONResponse(w, http.StatusOK, result)
-}
-
-// SaveTemplates handles saving templates
-func SaveTemplates(w http.ResponseWriter, r *http.Request) {
-       // Get user ID and derived key from context
-       userID, ok := r.Context().Value(utils.UserIDKey).(int)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
-               return
-       }
-       derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+       yearStr := r.URL.Query().Get("year")
+       if yearStr == "" {
+               http.Error(w, "Missing year parameter", http.StatusBadRequest)
                return
        }
-
-       // Parse request body
-       var req TemplatesRequest
-       if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
-               http.Error(w, "Invalid request body", http.StatusBadRequest)
+       year, err := strconv.Atoi(yearStr)
+       if err != nil {
+               http.Error(w, "Invalid year parameter", http.StatusBadRequest)
                return
        }
 
-       // Get encryption key
-       encKey, err := utils.GetEncryptionKey(userID, derivedKey)
+       // Get month data
+       content, err := utils.GetMonth(userID, year, month)
        if err != nil {
-               http.Error(w, fmt.Sprintf("Error getting encryption key: %v", err), http.StatusInternalServerError)
+               http.Error(w, fmt.Sprintf("Error retrieving month data: %v", err), http.StatusInternalServerError)
                return
        }
 
-       // Create new templates content
-       content := map[string]any{
-               "templates": []any{},
+       // Get or create days array
+       days, ok := content["days"].([]any)
+       if !ok {
+               days = []any{}
        }
 
-       // Encrypt template data
-       templates := []any{}
-       for _, template := range req.Templates {
-               encName, err := utils.EncryptText(template.Name, encKey)
-               if err != nil {
-                       http.Error(w, fmt.Sprintf("Error encrypting template name: %v", err), http.StatusInternalServerError)
-                       return
+       // Find day
+       dayFound := false
+       bookmarked := true
+       for i, dayInterface := range days {
+               dayObj, ok := dayInterface.(map[string]any)
+               if !ok {
+                       continue
                }
 
-               encText, err := utils.EncryptText(template.Text, encKey)
-               if err != nil {
-                       http.Error(w, fmt.Sprintf("Error encrypting template text: %v", err), http.StatusInternalServerError)
-                       return
+               dayNum, ok := dayObj["day"].(float64)
+               if !ok || int(dayNum) != day {
+                       continue
+               }
+
+               // Day found, toggle bookmark
+               dayFound = true
+               if bookmark, ok := dayObj["isBookmarked"].(bool); ok && bookmark {
+                       dayObj["isBookmarked"] = false
+                       bookmarked = false
+               } else {
+                       dayObj["isBookmarked"] = true
                }
+               days[i] = dayObj
+               break
+       }
 
-               templates = append(templates, map[string]any{
-                       "name": encName,
-                       "text": encText,
+       if !dayFound {
+               // Create new day with bookmark
+               days = append(days, map[string]any{
+                       "day":          day,
+                       "isBookmarked": true,
                })
        }
 
-       content["templates"] = templates
+       // Update days array
+       content["days"] = days
 
-       // Write templates
-       if err := utils.WriteTemplates(userID, content); err != nil {
-               http.Error(w, fmt.Sprintf("Error writing templates: %v", err), http.StatusInternalServerError)
+       // Write month data
+       if err := utils.WriteMonth(userID, year, month, content); err != nil {
+               http.Error(w, fmt.Sprintf("Failed to bookmark day - error writing log: %v", err), http.StatusInternalServerError)
                return
        }
 
        // Return success
-       utils.JSONResponse(w, http.StatusOK, map[string]bool{
-               "success": true,
+       utils.JSONResponse(w, http.StatusOK, map[string]any{
+               "success":    true,
+               "bookmarked": bookmarked,
        })
 }
 
@@ -1004,265 +964,3 @@ func DeleteDay(w http.ResponseWriter, r *http.Request) {
 
        utils.JSONResponse(w, http.StatusOK, map[string]bool{"success": true})
 }
-
-// RenameFileRequest represents the rename file request body
-type RenameFileRequest struct {
-       UUID        string `json:"uuid"`
-       NewFilename string `json:"new_filename"`
-       Day         int    `json:"day"`
-       Month       int    `json:"month"`
-       Year        int    `json:"year"`
-}
-
-// RenameFile handles renaming a file
-func RenameFile(w http.ResponseWriter, r *http.Request) {
-       // Get user ID from context
-       userID, ok := r.Context().Value(utils.UserIDKey).(int)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
-               return
-       }
-       derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
-               return
-       }
-
-       // Parse request body
-       var req RenameFileRequest
-       if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
-               http.Error(w, "Invalid request body", http.StatusBadRequest)
-               return
-       }
-
-       // Validate input
-       req.NewFilename = strings.TrimSpace(req.NewFilename)
-       if req.NewFilename == "" {
-               utils.JSONResponse(w, http.StatusBadRequest, map[string]any{
-                       "success": false,
-                       "message": "New filename cannot be empty",
-               })
-               return
-       }
-
-       if req.UUID == "" {
-               utils.JSONResponse(w, http.StatusBadRequest, map[string]any{
-                       "success": false,
-                       "message": "File UUID is required",
-               })
-               return
-       }
-
-       // Get month data
-       content, err := utils.GetMonth(userID, req.Year, req.Month)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error retrieving month data: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       encKey, err := utils.GetEncryptionKey(userID, derivedKey)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error getting encryption key: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       enc_filename, err := utils.EncryptText(req.NewFilename, encKey)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error encrypting text: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Find and update the file
-       days, ok := content["days"].([]any)
-       if !ok {
-               utils.JSONResponse(w, http.StatusNotFound, map[string]any{
-                       "success": false,
-                       "message": "No days found",
-               })
-               return
-       }
-
-       found := false
-       for _, d := range days {
-               day, ok := d.(map[string]any)
-               if !ok {
-                       continue
-               }
-
-               dayNum, ok := day["day"].(float64)
-               if !ok || int(dayNum) != req.Day {
-                       continue
-               }
-
-               files, ok := day["files"].([]any)
-               if !ok {
-                       continue
-               }
-
-               // Find and rename the specific file
-               for _, f := range files {
-                       file, ok := f.(map[string]any)
-                       if !ok {
-                               continue
-                       }
-
-                       if uuid, ok := file["uuid_filename"].(string); ok && uuid == req.UUID {
-                               file["enc_filename"] = enc_filename
-                               found = true
-                               break
-                       }
-               }
-
-               if found {
-                       break
-               }
-       }
-
-       if !found {
-               utils.JSONResponse(w, http.StatusNotFound, map[string]any{
-                       "success": false,
-                       "message": "File not found",
-               })
-               return
-       }
-
-       // Save the updated month data
-       if err := utils.WriteMonth(userID, req.Year, req.Month, content); err != nil {
-               http.Error(w, fmt.Sprintf("Error writing month data: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       utils.Logger.Printf("File renamed successfully for user %d: %s -> %s", userID, req.UUID, req.NewFilename)
-       utils.JSONResponse(w, http.StatusOK, map[string]bool{"success": true})
-}
-
-// ReorderFilesRequest represents the reorder files request body
-type ReorderFilesRequest struct {
-       Day       int            `json:"day"`
-       Month     int            `json:"month"`
-       Year      int            `json:"year"`
-       FileOrder map[string]int `json:"file_order"` // UUID -> order index
-}
-
-// ReorderFiles handles reordering files within a day
-func ReorderFiles(w http.ResponseWriter, r *http.Request) {
-       // Get user ID from context
-       userID, ok := r.Context().Value(utils.UserIDKey).(int)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
-               return
-       }
-
-       // Parse request body
-       var req ReorderFilesRequest
-       if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
-               http.Error(w, "Invalid request body", http.StatusBadRequest)
-               return
-       }
-
-       if len(req.FileOrder) == 0 {
-               utils.JSONResponse(w, http.StatusBadRequest, map[string]any{
-                       "success": false,
-                       "message": "File order mapping is required",
-               })
-               return
-       }
-
-       // Get month data
-       content, err := utils.GetMonth(userID, req.Year, req.Month)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error retrieving month data: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Find and reorder files for the specific day
-       days, ok := content["days"].([]any)
-       if !ok {
-               utils.JSONResponse(w, http.StatusNotFound, map[string]any{
-                       "success": false,
-                       "message": "No days found",
-               })
-               return
-       }
-
-       found := false
-       for _, d := range days {
-               day, ok := d.(map[string]any)
-               if !ok {
-                       continue
-               }
-
-               dayNum, ok := day["day"].(float64)
-               if !ok || int(dayNum) != req.Day {
-                       continue
-               }
-
-               files, ok := day["files"].([]any)
-               if !ok {
-                       continue
-               }
-
-               // Create a slice to hold files with their new order
-               type fileWithOrder struct {
-                       file  map[string]any
-                       order int
-               }
-
-               var orderedFiles []fileWithOrder
-
-               // Assign order to each file
-               for _, f := range files {
-                       file, ok := f.(map[string]any)
-                       if !ok {
-                               continue
-                       }
-
-                       uuid, ok := file["uuid_filename"].(string)
-                       if !ok {
-                               continue
-                       }
-
-                       if order, exists := req.FileOrder[uuid]; exists {
-                               orderedFiles = append(orderedFiles, fileWithOrder{file: file, order: order})
-                       } else {
-                               // Files not in the reorder map get appended at the end
-                               orderedFiles = append(orderedFiles, fileWithOrder{file: file, order: len(req.FileOrder)})
-                       }
-               }
-
-               // Sort files by their order
-               for i := 0; i < len(orderedFiles)-1; i++ {
-                       for j := i + 1; j < len(orderedFiles); j++ {
-                               if orderedFiles[i].order > orderedFiles[j].order {
-                                       orderedFiles[i], orderedFiles[j] = orderedFiles[j], orderedFiles[i]
-                               }
-                       }
-               }
-
-               // Update the files array with the new order
-               newFiles := make([]any, len(orderedFiles))
-               for i, fileWithOrder := range orderedFiles {
-                       newFiles[i] = fileWithOrder.file
-               }
-               day["files"] = newFiles
-
-               found = true
-               break
-       }
-
-       if !found {
-               utils.JSONResponse(w, http.StatusNotFound, map[string]any{
-                       "success": false,
-                       "message": "Day not found",
-               })
-               return
-       }
-
-       // Save the updated month data
-       if err := utils.WriteMonth(userID, req.Year, req.Month, content); err != nil {
-               http.Error(w, fmt.Sprintf("Error writing month data: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       utils.JSONResponse(w, http.StatusOK, map[string]bool{"success": true})
-}
similarity index 64%
rename from backend/handlers/additional.go
rename to backend/handlers/search.go
index d2d9d4d650ec9d93c31eb0a2a01f8769fe3cd599..b1a550b96f598de9038a91e5603981f2d9885eb1 100644 (file)
@@ -22,909 +22,6 @@ import (
        "github.com/phitux/dailytxt/backend/utils"
 )
 
-// EditTagRequest represents the edit tag request
-type EditTagRequest struct {
-       ID    int    `json:"id"`
-       Icon  string `json:"icon"`
-       Name  string `json:"name"`
-       Color string `json:"color"`
-}
-
-// EditTag handles editing a tag
-func EditTag(w http.ResponseWriter, r *http.Request) {
-       // Get user ID and derived key from context
-       userID, ok := r.Context().Value(utils.UserIDKey).(int)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
-               return
-       }
-       derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
-               return
-       }
-
-       // Parse request body
-       var req EditTagRequest
-       if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
-               http.Error(w, "Invalid request body", http.StatusBadRequest)
-               return
-       }
-
-       // Get tags
-       content, err := utils.GetTags(userID)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error retrieving tags: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Check if tags exist
-       tags, ok := content["tags"].([]any)
-       if !ok {
-               http.Error(w, "Tag not found - json error", http.StatusInternalServerError)
-               return
-       }
-
-       // Get encryption key
-       encKey, err := utils.GetEncryptionKey(userID, derivedKey)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error getting encryption key: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Find and update tag
-       found := false
-       for i, tagInterface := range tags {
-               tag, ok := tagInterface.(map[string]any)
-               if !ok {
-                       continue
-               }
-
-               if id, ok := tag["id"].(float64); ok && int(id) == req.ID {
-                       // Encrypt tag data
-                       encIcon, err := utils.EncryptText(req.Icon, encKey)
-                       if err != nil {
-                               http.Error(w, fmt.Sprintf("Error encrypting tag icon: %v", err), http.StatusInternalServerError)
-                               return
-                       }
-
-                       encName, err := utils.EncryptText(req.Name, encKey)
-                       if err != nil {
-                               http.Error(w, fmt.Sprintf("Error encrypting tag name: %v", err), http.StatusInternalServerError)
-                               return
-                       }
-
-                       encColor, err := utils.EncryptText(req.Color, encKey)
-                       if err != nil {
-                               http.Error(w, fmt.Sprintf("Error encrypting tag color: %v", err), http.StatusInternalServerError)
-                               return
-                       }
-
-                       // Update tag
-                       tag["icon"] = encIcon
-                       tag["name"] = encName
-                       tag["color"] = encColor
-                       tags[i] = tag
-                       found = true
-                       break
-               }
-       }
-
-       if !found {
-               http.Error(w, "Tag not found - not in tags", http.StatusInternalServerError)
-               return
-       }
-
-       // Write tags
-       if err := utils.WriteTags(userID, content); err != nil {
-               http.Error(w, fmt.Sprintf("Failed to write tag - error writing tags: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Return success
-       utils.JSONResponse(w, http.StatusOK, map[string]bool{
-               "success": true,
-       })
-}
-
-// DeleteTag handles deleting a tag
-func DeleteTag(w http.ResponseWriter, r *http.Request) {
-       // Get user ID from context
-       userID, ok := r.Context().Value(utils.UserIDKey).(int)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
-               return
-       }
-
-       // Get tag ID
-       idStr := r.URL.Query().Get("id")
-       if idStr == "" {
-               http.Error(w, "Missing id parameter", http.StatusBadRequest)
-               return
-       }
-       id, err := strconv.Atoi(idStr)
-       if err != nil {
-               http.Error(w, "Invalid id parameter", http.StatusBadRequest)
-               return
-       }
-
-       // Get all years and months
-       years, err := utils.GetYears(userID)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error retrieving years: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Remove tag from all logs
-       for _, year := range years {
-               yearInt, _ := strconv.Atoi(year)
-               months, err := utils.GetMonths(userID, year)
-               if err != nil {
-                       continue
-               }
-
-               for _, month := range months {
-                       monthInt, _ := strconv.Atoi(month)
-                       content, err := utils.GetMonth(userID, yearInt, monthInt)
-                       if err != nil {
-                               continue
-                       }
-
-                       days, ok := content["days"].([]any)
-                       if !ok {
-                               continue
-                       }
-
-                       // Check each day for the tag
-                       modified := false
-                       for i, dayInterface := range days {
-                               day, ok := dayInterface.(map[string]any)
-                               if !ok {
-                                       continue
-                               }
-
-                               tags, ok := day["tags"].([]any)
-                               if !ok {
-                                       continue
-                               }
-
-                               // Find and remove the tag
-                               for j, tagID := range tags {
-                                       if tagIDFloat, ok := tagID.(float64); ok && int(tagIDFloat) == id {
-                                               // Remove tag
-                                               tags = append(tags[:j], tags[j+1:]...)
-                                               day["tags"] = tags
-                                               days[i] = day
-                                               modified = true
-                                               break
-                                       }
-                               }
-                       }
-
-                       // Write updated month if modified
-                       if modified {
-                               content["days"] = days
-                               if err := utils.WriteMonth(userID, yearInt, monthInt, content); err != nil {
-                                       http.Error(w, fmt.Sprintf("Failed to delete tag - error writing log: %v", err), http.StatusInternalServerError)
-                                       return
-                               }
-                       }
-               }
-       }
-
-       // Get tags
-       content, err := utils.GetTags(userID)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error retrieving tags: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Check if tags exist
-       tags, ok := content["tags"].([]any)
-       if !ok {
-               http.Error(w, "Tag not found - json error", http.StatusInternalServerError)
-               return
-       }
-
-       // Find and remove tag
-       found := false
-       for i, tagInterface := range tags {
-               tag, ok := tagInterface.(map[string]any)
-               if !ok {
-                       continue
-               }
-
-               if tagID, ok := tag["id"].(float64); ok && int(tagID) == id {
-                       // Remove tag
-                       tags = append(tags[:i], tags[i+1:]...)
-                       content["tags"] = tags
-                       found = true
-                       break
-               }
-       }
-
-       if !found {
-               http.Error(w, "Tag not found - not in tags", http.StatusInternalServerError)
-               return
-       }
-
-       // Write tags
-       if err := utils.WriteTags(userID, content); err != nil {
-               http.Error(w, fmt.Sprintf("Failed to delete tag - error writing tags: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Return success
-       utils.JSONResponse(w, http.StatusOK, map[string]bool{
-               "success": true,
-       })
-}
-
-// TagLogRequest represents the tag log request
-type TagLogRequest struct {
-       Day   int `json:"day"`
-       Month int `json:"month"`
-       Year  int `json:"year"`
-       TagID int `json:"tag_id"`
-}
-
-// AddTagToLog handles adding a tag to a log
-func AddTagToLog(w http.ResponseWriter, r *http.Request) {
-       // Get user ID from context
-       userID, ok := r.Context().Value(utils.UserIDKey).(int)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
-               return
-       }
-
-       // Parse request body
-       var req TagLogRequest
-       if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
-               http.Error(w, "Invalid request body", http.StatusBadRequest)
-               return
-       }
-
-       // Get month data
-       content, err := utils.GetMonth(userID, req.Year, req.Month)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error retrieving month data: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Get or create days array
-       days, ok := content["days"].([]any)
-       if !ok {
-               days = []any{}
-       }
-
-       // Find day
-       dayFound := false
-       for i, dayInterface := range days {
-               day, ok := dayInterface.(map[string]any)
-               if !ok {
-                       continue
-               }
-
-               dayNum, ok := day["day"].(float64)
-               if !ok || int(dayNum) != req.Day {
-                       continue
-               }
-
-               // Day found, add tag
-               dayFound = true
-               tags, ok := day["tags"].([]any)
-               if !ok {
-                       tags = []any{}
-               }
-
-               // Check if tag already exists
-               tagExists := false
-               for _, tagID := range tags {
-                       if tagIDFloat, ok := tagID.(float64); ok && int(tagIDFloat) == req.TagID {
-                               tagExists = true
-                               break
-                       }
-               }
-
-               if !tagExists {
-                       tags = append(tags, float64(req.TagID))
-                       day["tags"] = tags
-                       days[i] = day
-               }
-               break
-       }
-
-       if !dayFound {
-               // Create new day with tag
-               days = append(days, map[string]any{
-                       "day":  req.Day,
-                       "tags": []any{float64(req.TagID)},
-               })
-       }
-
-       // Update days array
-       content["days"] = days
-
-       // Write month data
-       if err := utils.WriteMonth(userID, req.Year, req.Month, content); err != nil {
-               http.Error(w, fmt.Sprintf("Failed to write tag - error writing log: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Return success
-       utils.JSONResponse(w, http.StatusOK, map[string]bool{
-               "success": true,
-       })
-}
-
-// RemoveTagFromLog handles removing a tag from a log
-func RemoveTagFromLog(w http.ResponseWriter, r *http.Request) {
-       // Get user ID from context
-       userID, ok := r.Context().Value(utils.UserIDKey).(int)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
-               return
-       }
-
-       // Parse request body
-       var req TagLogRequest
-       if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
-               http.Error(w, "Invalid request body", http.StatusBadRequest)
-               return
-       }
-
-       // Get month data
-       content, err := utils.GetMonth(userID, req.Year, req.Month)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error retrieving month data: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Check if days exist
-       days, ok := content["days"].([]any)
-       if !ok {
-               http.Error(w, "Day not found - json error", http.StatusInternalServerError)
-               return
-       }
-
-       // Find day
-       found := false
-       for i, dayInterface := range days {
-               day, ok := dayInterface.(map[string]any)
-               if !ok {
-                       continue
-               }
-
-               dayNum, ok := day["day"].(float64)
-               if !ok || int(dayNum) != req.Day {
-                       continue
-               }
-
-               // Day found, check for tags
-               tags, ok := day["tags"].([]any)
-               if !ok {
-                       http.Error(w, "Failed to remove tag - not found in log", http.StatusInternalServerError)
-                       return
-               }
-
-               // Find and remove tag
-               for j, tagID := range tags {
-                       if tagIDFloat, ok := tagID.(float64); ok && int(tagIDFloat) == req.TagID {
-                               // Remove tag
-                               tags = append(tags[:j], tags[j+1:]...)
-                               day["tags"] = tags
-                               days[i] = day
-                               found = true
-                               break
-                       }
-               }
-
-               if !found {
-                       http.Error(w, "Failed to remove tag - not found in log", http.StatusInternalServerError)
-                       return
-               }
-               break
-       }
-
-       if !found {
-               http.Error(w, "Failed to remove tag - not found in log", http.StatusInternalServerError)
-               return
-       }
-
-       // Update days array
-       content["days"] = days
-
-       // Write month data
-       if err := utils.WriteMonth(userID, req.Year, req.Month, content); err != nil {
-               http.Error(w, fmt.Sprintf("Failed to remove tag - error writing log: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Return success
-       utils.JSONResponse(w, http.StatusOK, map[string]bool{
-               "success": true,
-       })
-}
-
-// UploadFile handles uploading a file
-func UploadFile(w http.ResponseWriter, r *http.Request) {
-       // Get user ID and derived key from context
-       userID, ok := r.Context().Value(utils.UserIDKey).(int)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
-               return
-       }
-       derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
-               return
-       }
-
-       // Parse form
-       if err := r.ParseMultipartForm(10 << 20); err != nil { // 10 MB max
-               http.Error(w, fmt.Sprintf("Error parsing form: %v", err), http.StatusBadRequest)
-               return
-       }
-
-       // Get form values
-       dayStr := r.FormValue("day")
-       if dayStr == "" {
-               http.Error(w, "Missing day parameter", http.StatusBadRequest)
-               return
-       }
-       day, err := strconv.Atoi(dayStr)
-       if err != nil {
-               http.Error(w, "Invalid day parameter", http.StatusBadRequest)
-               return
-       }
-
-       monthStr := r.FormValue("month")
-       if monthStr == "" {
-               http.Error(w, "Missing month parameter", http.StatusBadRequest)
-               return
-       }
-       month, err := strconv.Atoi(monthStr)
-       if err != nil {
-               http.Error(w, "Invalid month parameter", http.StatusBadRequest)
-               return
-       }
-
-       yearStr := r.FormValue("year")
-       if yearStr == "" {
-               http.Error(w, "Missing year parameter", http.StatusBadRequest)
-               return
-       }
-       year, err := strconv.Atoi(yearStr)
-       if err != nil {
-               http.Error(w, "Invalid year parameter", http.StatusBadRequest)
-               return
-       }
-
-       uuid := r.FormValue("uuid")
-       if uuid == "" {
-               http.Error(w, "Missing uuid parameter", http.StatusBadRequest)
-               return
-       }
-
-       // Get file
-       file, header, err := r.FormFile("file")
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error getting file: %v", err), http.StatusBadRequest)
-               return
-       }
-       defer file.Close()
-
-       // Get encryption key first (before reading large file)
-       encKey, err := utils.GetEncryptionKey(userID, derivedKey)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error getting encryption key: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Read file into a buffer (more memory efficient)
-       fileBytes, err := io.ReadAll(file)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError)
-               return
-       }
-       // Ensure fileBytes is cleared when function exits
-       defer func() { fileBytes = nil }()
-
-       // Encrypt file
-       encryptedFile, err := utils.EncryptFile(fileBytes, encKey)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error encrypting file: %v", err), http.StatusInternalServerError)
-               return
-       }
-       // Ensure encryptedFile is cleared when function exits
-       defer func() { encryptedFile = nil }()
-
-       // Clear original file data from memory immediately after encryption
-       fileBytes = nil
-
-       // Write file
-       if err := utils.WriteFile(encryptedFile, userID, uuid); err != nil {
-               http.Error(w, fmt.Sprintf("Error writing file: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Clear encrypted data from memory immediately after writing
-       encryptedFile = nil
-
-       // Get month data
-       content, err := utils.GetMonth(userID, year, month)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error retrieving month data: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Encrypt filename
-       encFilename, err := utils.EncryptText(header.Filename, encKey)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error encrypting filename: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Create new file entry
-       newFile := map[string]any{
-               "enc_filename":  encFilename,
-               "uuid_filename": uuid,
-               "size":          header.Size,
-       }
-
-       // Add file to day
-       days, ok := content["days"].([]any)
-       if !ok {
-               days = []any{}
-       }
-
-       dayFound := false
-       for i, dayInterface := range days {
-               dayObj, ok := dayInterface.(map[string]any)
-               if !ok {
-                       continue
-               }
-
-               dayNum, ok := dayObj["day"].(float64)
-               if !ok || int(dayNum) != day {
-                       continue
-               }
-
-               // Add file to existing day
-               dayFound = true
-               files, ok := dayObj["files"].([]any)
-               if !ok {
-                       files = []any{}
-               }
-               files = append(files, newFile)
-               dayObj["files"] = files
-               days[i] = dayObj
-               break
-       }
-
-       if !dayFound {
-               // Create new day with file
-               days = append(days, map[string]any{
-                       "day":   day,
-                       "files": []any{newFile},
-               })
-       }
-
-       // Update days array
-       content["days"] = days
-
-       // Write month data
-       if err := utils.WriteMonth(userID, year, month, content); err != nil {
-               // Cleanup on error
-               utils.RemoveFile(userID, uuid)
-               http.Error(w, fmt.Sprintf("Error writing month data: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Return success
-       utils.JSONResponse(w, http.StatusOK, map[string]bool{
-               "success": true,
-       })
-}
-
-// DownloadFile handles downloading a file
-func DownloadFile(w http.ResponseWriter, r *http.Request) {
-       // Get user ID and derived key from context
-       userID, ok := r.Context().Value(utils.UserIDKey).(int)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
-               return
-       }
-       derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
-               return
-       }
-
-       // Get uuid parameter
-       uuid := r.URL.Query().Get("uuid")
-       if uuid == "" {
-               http.Error(w, "Missing uuid parameter", http.StatusBadRequest)
-               return
-       }
-
-       // Get encryption key
-       encKey, err := utils.GetEncryptionKey(userID, derivedKey)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error getting encryption key: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Read file
-       encryptedFile, err := utils.ReadFile(userID, uuid)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError)
-               return
-       }
-       // Ensure encryptedFile is cleared when function exits
-       defer func() { encryptedFile = nil }()
-
-       // Decrypt file
-       decryptedFile, err := utils.DecryptFile(encryptedFile, encKey)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error decrypting file: %v", err), http.StatusInternalServerError)
-               return
-       }
-       // Ensure decryptedFile is cleared when function exits
-       defer func() { decryptedFile = nil }()
-
-       // Clear encrypted data from memory immediately after decryption
-       encryptedFile = nil
-
-       // Set response headers for streaming
-       w.Header().Set("Content-Type", "application/octet-stream")
-       w.Header().Set("Content-Disposition", "attachment")
-
-       // Write file to response
-       if _, err := w.Write(decryptedFile); err != nil {
-               http.Error(w, fmt.Sprintf("Error writing response: %v", err), http.StatusInternalServerError)
-               return
-       }
-}
-
-// DeleteFile handles deleting a file
-func DeleteFile(w http.ResponseWriter, r *http.Request) {
-       // Get user ID from context
-       userID, ok := r.Context().Value(utils.UserIDKey).(int)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
-               return
-       }
-
-       // Get parameters
-       uuid := r.URL.Query().Get("uuid")
-       if uuid == "" {
-               http.Error(w, "Missing uuid parameter", http.StatusBadRequest)
-               return
-       }
-
-       dayStr := r.URL.Query().Get("day")
-       if dayStr == "" {
-               http.Error(w, "Missing day parameter", http.StatusBadRequest)
-               return
-       }
-       day, err := strconv.Atoi(dayStr)
-       if err != nil {
-               http.Error(w, "Invalid day parameter", http.StatusBadRequest)
-               return
-       }
-
-       monthStr := r.URL.Query().Get("month")
-       if monthStr == "" {
-               http.Error(w, "Missing month parameter", http.StatusBadRequest)
-               return
-       }
-       month, err := strconv.Atoi(monthStr)
-       if err != nil {
-               http.Error(w, "Invalid month parameter", http.StatusBadRequest)
-               return
-       }
-
-       yearStr := r.URL.Query().Get("year")
-       if yearStr == "" {
-               http.Error(w, "Missing year parameter", http.StatusBadRequest)
-               return
-       }
-       year, err := strconv.Atoi(yearStr)
-       if err != nil {
-               http.Error(w, "Invalid year parameter", http.StatusBadRequest)
-               return
-       }
-
-       // Get month data
-       content, err := utils.GetMonth(userID, year, month)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error retrieving month data: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Check if days exist
-       days, ok := content["days"].([]any)
-       if !ok {
-               http.Error(w, "Day not found - json error", http.StatusInternalServerError)
-               return
-       }
-
-       // Find day and file
-       fileFound := false
-       for i, dayInterface := range days {
-               dayObj, ok := dayInterface.(map[string]any)
-               if !ok {
-                       continue
-               }
-
-               dayNum, ok := dayObj["day"].(float64)
-               if !ok || int(dayNum) != day {
-                       continue
-               }
-
-               // Check for files
-               files, ok := dayObj["files"].([]any)
-               if !ok {
-                       continue
-               }
-
-               // Find file
-               for j, fileInterface := range files {
-                       file, ok := fileInterface.(map[string]any)
-                       if !ok {
-                               continue
-                       }
-
-                       uuidFilename, ok := file["uuid_filename"].(string)
-                       if !ok || uuidFilename != uuid {
-                               continue
-                       }
-
-                       // Remove file from array
-                       if err := utils.RemoveFile(userID, uuid); err != nil {
-                               http.Error(w, fmt.Sprintf("Failed to delete file: %v", err), http.StatusInternalServerError)
-                               return
-                       }
-
-                       files = append(files[:j], files[j+1:]...)
-                       dayObj["files"] = files
-                       days[i] = dayObj
-                       fileFound = true
-                       break
-               }
-
-               if fileFound {
-                       break
-               }
-       }
-
-       if !fileFound {
-               http.Error(w, "Failed to delete file - not found in log", http.StatusInternalServerError)
-               return
-       }
-
-       // Update days array
-       content["days"] = days
-
-       // Write month data
-       if err := utils.WriteMonth(userID, year, month, content); err != nil {
-               http.Error(w, fmt.Sprintf("Failed to write changes of deleted file: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Return success
-       utils.JSONResponse(w, http.StatusOK, map[string]bool{
-               "success": true,
-       })
-}
-
-// BookmarkDay handles bookmarking a day
-func BookmarkDay(w http.ResponseWriter, r *http.Request) {
-       // Get user ID from context
-       userID, ok := r.Context().Value(utils.UserIDKey).(int)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
-               return
-       }
-
-       // Get parameters
-       dayStr := r.URL.Query().Get("day")
-       if dayStr == "" {
-               http.Error(w, "Missing day parameter", http.StatusBadRequest)
-               return
-       }
-       day, err := strconv.Atoi(dayStr)
-       if err != nil {
-               http.Error(w, "Invalid day parameter", http.StatusBadRequest)
-               return
-       }
-
-       monthStr := r.URL.Query().Get("month")
-       if monthStr == "" {
-               http.Error(w, "Missing month parameter", http.StatusBadRequest)
-               return
-       }
-       month, err := strconv.Atoi(monthStr)
-       if err != nil {
-               http.Error(w, "Invalid month parameter", http.StatusBadRequest)
-               return
-       }
-
-       yearStr := r.URL.Query().Get("year")
-       if yearStr == "" {
-               http.Error(w, "Missing year parameter", http.StatusBadRequest)
-               return
-       }
-       year, err := strconv.Atoi(yearStr)
-       if err != nil {
-               http.Error(w, "Invalid year parameter", http.StatusBadRequest)
-               return
-       }
-
-       // Get month data
-       content, err := utils.GetMonth(userID, year, month)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error retrieving month data: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Get or create days array
-       days, ok := content["days"].([]any)
-       if !ok {
-               days = []any{}
-       }
-
-       // Find day
-       dayFound := false
-       bookmarked := true
-       for i, dayInterface := range days {
-               dayObj, ok := dayInterface.(map[string]any)
-               if !ok {
-                       continue
-               }
-
-               dayNum, ok := dayObj["day"].(float64)
-               if !ok || int(dayNum) != day {
-                       continue
-               }
-
-               // Day found, toggle bookmark
-               dayFound = true
-               if bookmark, ok := dayObj["isBookmarked"].(bool); ok && bookmark {
-                       dayObj["isBookmarked"] = false
-                       bookmarked = false
-               } else {
-                       dayObj["isBookmarked"] = true
-               }
-               days[i] = dayObj
-               break
-       }
-
-       if !dayFound {
-               // Create new day with bookmark
-               days = append(days, map[string]any{
-                       "day":          day,
-                       "isBookmarked": true,
-               })
-       }
-
-       // Update days array
-       content["days"] = days
-
-       // Write month data
-       if err := utils.WriteMonth(userID, year, month, content); err != nil {
-               http.Error(w, fmt.Sprintf("Failed to bookmark day - error writing log: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Return success
-       utils.JSONResponse(w, http.StatusOK, map[string]any{
-               "success":    true,
-               "bookmarked": bookmarked,
-       })
-}
-
 // SearchTag handles searching logs by tag
 func SearchTag(w http.ResponseWriter, r *http.Request) {
        // Get user ID and derived key from context
@@ -1071,211 +168,6 @@ func SearchTag(w http.ResponseWriter, r *http.Request) {
        utils.JSONResponse(w, http.StatusOK, results)
 }
 
-// GetTags handles retrieving a user's tags
-func GetTags(w http.ResponseWriter, r *http.Request) {
-       // Get user ID and derived key from context
-       userID, ok := r.Context().Value(utils.UserIDKey).(int)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
-               return
-       }
-       derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
-               return
-       }
-
-       // Get tags
-       content, err := utils.GetTags(userID)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error retrieving tags: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // If no tags, return empty array
-       if tags, ok := content["tags"].([]any); !ok || len(tags) == 0 {
-               utils.JSONResponse(w, http.StatusOK, []any{})
-               return
-       }
-
-       // Get encryption key
-       encKey, err := utils.GetEncryptionKey(userID, derivedKey)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error getting encryption key: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Decrypt tag data
-       tags := content["tags"].([]any)
-       result := []any{}
-
-       for _, tagInterface := range tags {
-               tag, ok := tagInterface.(map[string]any)
-               if !ok {
-                       continue
-               }
-
-               // Decrypt icon, name, and color
-               if encIcon, ok := tag["icon"].(string); ok {
-                       decryptedIcon, err := utils.DecryptText(encIcon, encKey)
-                       if err != nil {
-                               http.Error(w, fmt.Sprintf("Error decrypting tag icon: %v", err), http.StatusInternalServerError)
-                               return
-                       }
-                       tag["icon"] = decryptedIcon
-               }
-
-               if encName, ok := tag["name"].(string); ok {
-                       decryptedName, err := utils.DecryptText(encName, encKey)
-                       if err != nil {
-                               http.Error(w, fmt.Sprintf("Error decrypting tag name: %v", err), http.StatusInternalServerError)
-                               return
-                       }
-                       tag["name"] = decryptedName
-               }
-
-               if encColor, ok := tag["color"].(string); ok {
-                       decryptedColor, err := utils.DecryptText(encColor, encKey)
-                       if err != nil {
-                               http.Error(w, fmt.Sprintf("Error decrypting tag color: %v", err), http.StatusInternalServerError)
-                               return
-                       }
-                       tag["color"] = decryptedColor
-               }
-
-               result = append(result, tag)
-       }
-
-       // Return tags
-       utils.JSONResponse(w, http.StatusOK, result)
-}
-
-// TagRequest represents a tag request
-type TagRequest struct {
-       Icon  string `json:"icon"`
-       Name  string `json:"name"`
-       Color string `json:"color"`
-}
-
-// SaveTags handles saving a new tag
-func SaveTags(w http.ResponseWriter, r *http.Request) {
-       // Get user ID and derived key from context
-       userID, ok := r.Context().Value(utils.UserIDKey).(int)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
-               return
-       }
-       derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
-               return
-       }
-
-       // Parse request body
-       var req TagRequest
-       if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
-               http.Error(w, "Invalid request body", http.StatusBadRequest)
-               return
-       }
-
-       // Get tags
-       content, err := utils.GetTags(userID)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error retrieving tags: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Create tags array if it doesn't exist
-       if _, ok := content["tags"]; !ok {
-               content["tags"] = []any{}
-       }
-       if _, ok := content["next_id"]; !ok {
-               content["next_id"] = 1
-       }
-
-       // Get encryption key
-       encKey, err := utils.GetEncryptionKey(userID, derivedKey)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error getting encryption key: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Check for duplicate tag names
-       tags, ok := content["tags"].([]any)
-       if ok {
-               for _, tagInterface := range tags {
-                       tag, ok := tagInterface.(map[string]any)
-                       if !ok {
-                               continue
-                       }
-
-                       if encName, ok := tag["name"].(string); ok {
-                               decryptedName, err := utils.DecryptText(encName, encKey)
-                               if err != nil {
-                                       http.Error(w, fmt.Sprintf("Error decrypting tag name: %v", err), http.StatusInternalServerError)
-                                       return
-                               }
-                               if decryptedName == req.Name {
-                                       http.Error(w, "Tag name already exists", http.StatusBadRequest)
-                                       return
-                               }
-                       }
-               }
-       }
-
-       // Encrypt tag data
-       encIcon, err := utils.EncryptText(req.Icon, encKey)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error encrypting tag icon: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       encName, err := utils.EncryptText(req.Name, encKey)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error encrypting tag name: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       encColor, err := utils.EncryptText(req.Color, encKey)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error encrypting tag color: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Create new tag
-       nextID, ok := content["next_id"].(float64)
-       if !ok {
-               nextID = 1
-       }
-
-       newTag := map[string]any{
-               "id":    int(nextID),
-               "icon":  encIcon,
-               "name":  encName,
-               "color": encColor,
-       }
-
-       // Add tag to tags array
-       tags, ok = content["tags"].([]any)
-       if !ok {
-               tags = []any{}
-       }
-       tags = append(tags, newTag)
-       content["tags"] = tags
-       content["next_id"] = nextID + 1
-
-       // Write tags
-       if err := utils.WriteTags(userID, content); err != nil {
-               http.Error(w, fmt.Sprintf("Error writing tags: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Return success
-       utils.JSONResponse(w, http.StatusOK, map[string]bool{
-               "success": true,
-       })
-}
-
 // Helper functions for search
 func getStartIndex(text string, index int) int {
        if index == 0 {
diff --git a/backend/handlers/settings.go b/backend/handlers/settings.go
new file mode 100644 (file)
index 0000000..6aa3ebe
--- /dev/null
@@ -0,0 +1,182 @@
+package handlers
+
+import (
+       "encoding/json"
+       "fmt"
+       "maps"
+       "net/http"
+
+       "github.com/phitux/dailytxt/backend/utils"
+)
+
+func GetDefaultSettings() map[string]any {
+       // Default settings
+       return map[string]any{
+               "autoloadImagesByDefault":    false,
+               "setAutoloadImagesPerDevice": true,
+               "useALookBack":               true,
+               "aLookBackYears":             []int{1, 5, 10},
+               "useBrowserTimezone":         true,
+               "timezone":                   "UTC",
+               "useBrowserLanguage":         true,
+               "language":                   "en",
+               "darkModeAutoDetect":         true,
+               "useDarkMode":                false,
+               "background":                 "gradient",
+               "monochromeBackgroundColor":  "#ececec",
+               "checkForUpdates":            true,
+               "includeTestVersions":        false,
+               "requirePasswordOnPageLoad":  false,
+       }
+}
+
+// GetUserSettings retrieves user settings
+func GetUserSettings(w http.ResponseWriter, r *http.Request) {
+       // Get user ID from context
+       userID, ok := r.Context().Value(utils.UserIDKey).(int)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+
+       // Get derived key from context
+       derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+
+       // Get user settings
+       encryptedSettings, err := utils.GetUserSettings(userID)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error retrieving user settings: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Default settings
+       defaultSettings := GetDefaultSettings()
+
+       // If no settings found, return defaults
+       if len(encryptedSettings) == 0 {
+               utils.JSONResponse(w, http.StatusOK, defaultSettings)
+               return
+       }
+
+       // Decrypt settings
+       encKey, err := utils.GetEncryptionKey(userID, derivedKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error getting encryption key: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       decryptedSettings, err := utils.DecryptText(encryptedSettings, encKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error decrypting settings: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Parse JSON
+       var settings map[string]any
+       if err := json.Unmarshal([]byte(decryptedSettings), &settings); err != nil {
+               http.Error(w, fmt.Sprintf("Error parsing settings: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Apply defaults for missing keys
+       for key, value := range defaultSettings {
+               if _, exists := settings[key]; !exists {
+                       settings[key] = value
+               }
+       }
+
+       // Return settings
+       utils.JSONResponse(w, http.StatusOK, settings)
+}
+
+// SaveUserSettings saves user settings
+func SaveUserSettings(w http.ResponseWriter, r *http.Request) {
+       // Get user ID from context
+       userID, ok := r.Context().Value(utils.UserIDKey).(int)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+
+       // Get derived key from context
+       derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+
+       // Parse request body
+       var newSettings map[string]any
+       if err := json.NewDecoder(r.Body).Decode(&newSettings); err != nil {
+               http.Error(w, "Invalid request body", http.StatusBadRequest)
+               return
+       }
+
+       // Get existing settings
+       encryptedSettings, err := utils.GetUserSettings(userID)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error retrieving user settings: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Get encryption key
+       encKey, err := utils.GetEncryptionKey(userID, derivedKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error getting encryption key: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Current settings
+       var currentSettings map[string]any
+
+       // If settings exist, decrypt them
+       if len(encryptedSettings) > 0 {
+               decryptedSettings, err := utils.DecryptText(encryptedSettings, encKey)
+               if err != nil {
+                       http.Error(w, fmt.Sprintf("Error decrypting settings: %v", err), http.StatusInternalServerError)
+                       return
+               }
+
+               // Parse JSON
+               if err := json.Unmarshal([]byte(decryptedSettings), &currentSettings); err != nil {
+                       http.Error(w, fmt.Sprintf("Error parsing settings: %v", err), http.StatusInternalServerError)
+                       return
+               }
+       }
+
+       // If no settings or empty, use defaults
+       if len(currentSettings) == 0 {
+               currentSettings = GetDefaultSettings()
+       }
+
+       // Update settings
+       maps.Copy(currentSettings, newSettings)
+
+       // Encrypt settings
+       settingsJSON, err := json.Marshal(currentSettings)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error encoding settings: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       encryptedNewSettings, err := utils.EncryptText(string(settingsJSON), encKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error encrypting settings: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Write settings
+       if err := utils.WriteUserSettings(userID, encryptedNewSettings); err != nil {
+               http.Error(w, fmt.Sprintf("Error writing settings: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Return success
+       utils.JSONResponse(w, http.StatusOK, map[string]bool{
+               "success": true,
+       })
+}
diff --git a/backend/handlers/tags.go b/backend/handlers/tags.go
new file mode 100644 (file)
index 0000000..25b849f
--- /dev/null
@@ -0,0 +1,639 @@
+package handlers
+
+import (
+       "encoding/json"
+       "fmt"
+       "net/http"
+       "strconv"
+
+       "github.com/phitux/dailytxt/backend/utils"
+)
+
+// EditTagRequest represents the edit tag request
+type EditTagRequest struct {
+       ID    int    `json:"id"`
+       Icon  string `json:"icon"`
+       Name  string `json:"name"`
+       Color string `json:"color"`
+}
+
+// EditTag handles editing a tag
+func EditTag(w http.ResponseWriter, r *http.Request) {
+       // Get user ID and derived key from context
+       userID, ok := r.Context().Value(utils.UserIDKey).(int)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+       derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+
+       // Parse request body
+       var req EditTagRequest
+       if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+               http.Error(w, "Invalid request body", http.StatusBadRequest)
+               return
+       }
+
+       // Get tags
+       content, err := utils.GetTags(userID)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error retrieving tags: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Check if tags exist
+       tags, ok := content["tags"].([]any)
+       if !ok {
+               http.Error(w, "Tag not found - json error", http.StatusInternalServerError)
+               return
+       }
+
+       // Get encryption key
+       encKey, err := utils.GetEncryptionKey(userID, derivedKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error getting encryption key: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Find and update tag
+       found := false
+       for i, tagInterface := range tags {
+               tag, ok := tagInterface.(map[string]any)
+               if !ok {
+                       continue
+               }
+
+               if id, ok := tag["id"].(float64); ok && int(id) == req.ID {
+                       // Encrypt tag data
+                       encIcon, err := utils.EncryptText(req.Icon, encKey)
+                       if err != nil {
+                               http.Error(w, fmt.Sprintf("Error encrypting tag icon: %v", err), http.StatusInternalServerError)
+                               return
+                       }
+
+                       encName, err := utils.EncryptText(req.Name, encKey)
+                       if err != nil {
+                               http.Error(w, fmt.Sprintf("Error encrypting tag name: %v", err), http.StatusInternalServerError)
+                               return
+                       }
+
+                       encColor, err := utils.EncryptText(req.Color, encKey)
+                       if err != nil {
+                               http.Error(w, fmt.Sprintf("Error encrypting tag color: %v", err), http.StatusInternalServerError)
+                               return
+                       }
+
+                       // Update tag
+                       tag["icon"] = encIcon
+                       tag["name"] = encName
+                       tag["color"] = encColor
+                       tags[i] = tag
+                       found = true
+                       break
+               }
+       }
+
+       if !found {
+               http.Error(w, "Tag not found - not in tags", http.StatusInternalServerError)
+               return
+       }
+
+       // Write tags
+       if err := utils.WriteTags(userID, content); err != nil {
+               http.Error(w, fmt.Sprintf("Failed to write tag - error writing tags: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Return success
+       utils.JSONResponse(w, http.StatusOK, map[string]bool{
+               "success": true,
+       })
+}
+
+// DeleteTag handles deleting a tag
+func DeleteTag(w http.ResponseWriter, r *http.Request) {
+       // Get user ID from context
+       userID, ok := r.Context().Value(utils.UserIDKey).(int)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+
+       // Get tag ID
+       idStr := r.URL.Query().Get("id")
+       if idStr == "" {
+               http.Error(w, "Missing id parameter", http.StatusBadRequest)
+               return
+       }
+       id, err := strconv.Atoi(idStr)
+       if err != nil {
+               http.Error(w, "Invalid id parameter", http.StatusBadRequest)
+               return
+       }
+
+       // Get all years and months
+       years, err := utils.GetYears(userID)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error retrieving years: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Remove tag from all logs
+       for _, year := range years {
+               yearInt, _ := strconv.Atoi(year)
+               months, err := utils.GetMonths(userID, year)
+               if err != nil {
+                       continue
+               }
+
+               for _, month := range months {
+                       monthInt, _ := strconv.Atoi(month)
+                       content, err := utils.GetMonth(userID, yearInt, monthInt)
+                       if err != nil {
+                               continue
+                       }
+
+                       days, ok := content["days"].([]any)
+                       if !ok {
+                               continue
+                       }
+
+                       // Check each day for the tag
+                       modified := false
+                       for i, dayInterface := range days {
+                               day, ok := dayInterface.(map[string]any)
+                               if !ok {
+                                       continue
+                               }
+
+                               tags, ok := day["tags"].([]any)
+                               if !ok {
+                                       continue
+                               }
+
+                               // Find and remove the tag
+                               for j, tagID := range tags {
+                                       if tagIDFloat, ok := tagID.(float64); ok && int(tagIDFloat) == id {
+                                               // Remove tag
+                                               tags = append(tags[:j], tags[j+1:]...)
+                                               day["tags"] = tags
+                                               days[i] = day
+                                               modified = true
+                                               break
+                                       }
+                               }
+                       }
+
+                       // Write updated month if modified
+                       if modified {
+                               content["days"] = days
+                               if err := utils.WriteMonth(userID, yearInt, monthInt, content); err != nil {
+                                       http.Error(w, fmt.Sprintf("Failed to delete tag - error writing log: %v", err), http.StatusInternalServerError)
+                                       return
+                               }
+                       }
+               }
+       }
+
+       // Get tags
+       content, err := utils.GetTags(userID)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error retrieving tags: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Check if tags exist
+       tags, ok := content["tags"].([]any)
+       if !ok {
+               http.Error(w, "Tag not found - json error", http.StatusInternalServerError)
+               return
+       }
+
+       // Find and remove tag
+       found := false
+       for i, tagInterface := range tags {
+               tag, ok := tagInterface.(map[string]any)
+               if !ok {
+                       continue
+               }
+
+               if tagID, ok := tag["id"].(float64); ok && int(tagID) == id {
+                       // Remove tag
+                       tags = append(tags[:i], tags[i+1:]...)
+                       content["tags"] = tags
+                       found = true
+                       break
+               }
+       }
+
+       if !found {
+               http.Error(w, "Tag not found - not in tags", http.StatusInternalServerError)
+               return
+       }
+
+       // Write tags
+       if err := utils.WriteTags(userID, content); err != nil {
+               http.Error(w, fmt.Sprintf("Failed to delete tag - error writing tags: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Return success
+       utils.JSONResponse(w, http.StatusOK, map[string]bool{
+               "success": true,
+       })
+}
+
+// TagLogRequest represents the tag log request
+type TagLogRequest struct {
+       Day   int `json:"day"`
+       Month int `json:"month"`
+       Year  int `json:"year"`
+       TagID int `json:"tag_id"`
+}
+
+// AddTagToLog handles adding a tag to a log
+func AddTagToLog(w http.ResponseWriter, r *http.Request) {
+       // Get user ID from context
+       userID, ok := r.Context().Value(utils.UserIDKey).(int)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+
+       // Parse request body
+       var req TagLogRequest
+       if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+               http.Error(w, "Invalid request body", http.StatusBadRequest)
+               return
+       }
+
+       // Get month data
+       content, err := utils.GetMonth(userID, req.Year, req.Month)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error retrieving month data: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Get or create days array
+       days, ok := content["days"].([]any)
+       if !ok {
+               days = []any{}
+       }
+
+       // Find day
+       dayFound := false
+       for i, dayInterface := range days {
+               day, ok := dayInterface.(map[string]any)
+               if !ok {
+                       continue
+               }
+
+               dayNum, ok := day["day"].(float64)
+               if !ok || int(dayNum) != req.Day {
+                       continue
+               }
+
+               // Day found, add tag
+               dayFound = true
+               tags, ok := day["tags"].([]any)
+               if !ok {
+                       tags = []any{}
+               }
+
+               // Check if tag already exists
+               tagExists := false
+               for _, tagID := range tags {
+                       if tagIDFloat, ok := tagID.(float64); ok && int(tagIDFloat) == req.TagID {
+                               tagExists = true
+                               break
+                       }
+               }
+
+               if !tagExists {
+                       tags = append(tags, float64(req.TagID))
+                       day["tags"] = tags
+                       days[i] = day
+               }
+               break
+       }
+
+       if !dayFound {
+               // Create new day with tag
+               days = append(days, map[string]any{
+                       "day":  req.Day,
+                       "tags": []any{float64(req.TagID)},
+               })
+       }
+
+       // Update days array
+       content["days"] = days
+
+       // Write month data
+       if err := utils.WriteMonth(userID, req.Year, req.Month, content); err != nil {
+               http.Error(w, fmt.Sprintf("Failed to write tag - error writing log: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Return success
+       utils.JSONResponse(w, http.StatusOK, map[string]bool{
+               "success": true,
+       })
+}
+
+// RemoveTagFromLog handles removing a tag from a log
+func RemoveTagFromLog(w http.ResponseWriter, r *http.Request) {
+       // Get user ID from context
+       userID, ok := r.Context().Value(utils.UserIDKey).(int)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+
+       // Parse request body
+       var req TagLogRequest
+       if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+               http.Error(w, "Invalid request body", http.StatusBadRequest)
+               return
+       }
+
+       // Get month data
+       content, err := utils.GetMonth(userID, req.Year, req.Month)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error retrieving month data: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Check if days exist
+       days, ok := content["days"].([]any)
+       if !ok {
+               http.Error(w, "Day not found - json error", http.StatusInternalServerError)
+               return
+       }
+
+       // Find day
+       found := false
+       for i, dayInterface := range days {
+               day, ok := dayInterface.(map[string]any)
+               if !ok {
+                       continue
+               }
+
+               dayNum, ok := day["day"].(float64)
+               if !ok || int(dayNum) != req.Day {
+                       continue
+               }
+
+               // Day found, check for tags
+               tags, ok := day["tags"].([]any)
+               if !ok {
+                       http.Error(w, "Failed to remove tag - not found in log", http.StatusInternalServerError)
+                       return
+               }
+
+               // Find and remove tag
+               for j, tagID := range tags {
+                       if tagIDFloat, ok := tagID.(float64); ok && int(tagIDFloat) == req.TagID {
+                               // Remove tag
+                               tags = append(tags[:j], tags[j+1:]...)
+                               day["tags"] = tags
+                               days[i] = day
+                               found = true
+                               break
+                       }
+               }
+
+               if !found {
+                       http.Error(w, "Failed to remove tag - not found in log", http.StatusInternalServerError)
+                       return
+               }
+               break
+       }
+
+       if !found {
+               http.Error(w, "Failed to remove tag - not found in log", http.StatusInternalServerError)
+               return
+       }
+
+       // Update days array
+       content["days"] = days
+
+       // Write month data
+       if err := utils.WriteMonth(userID, req.Year, req.Month, content); err != nil {
+               http.Error(w, fmt.Sprintf("Failed to remove tag - error writing log: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Return success
+       utils.JSONResponse(w, http.StatusOK, map[string]bool{
+               "success": true,
+       })
+}
+
+// GetTags handles retrieving a user's tags
+func GetTags(w http.ResponseWriter, r *http.Request) {
+       // Get user ID and derived key from context
+       userID, ok := r.Context().Value(utils.UserIDKey).(int)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+       derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+
+       // Get tags
+       content, err := utils.GetTags(userID)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error retrieving tags: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // If no tags, return empty array
+       if tags, ok := content["tags"].([]any); !ok || len(tags) == 0 {
+               utils.JSONResponse(w, http.StatusOK, []any{})
+               return
+       }
+
+       // Get encryption key
+       encKey, err := utils.GetEncryptionKey(userID, derivedKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error getting encryption key: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Decrypt tag data
+       tags := content["tags"].([]any)
+       result := []any{}
+
+       for _, tagInterface := range tags {
+               tag, ok := tagInterface.(map[string]any)
+               if !ok {
+                       continue
+               }
+
+               // Decrypt icon, name, and color
+               if encIcon, ok := tag["icon"].(string); ok {
+                       decryptedIcon, err := utils.DecryptText(encIcon, encKey)
+                       if err != nil {
+                               http.Error(w, fmt.Sprintf("Error decrypting tag icon: %v", err), http.StatusInternalServerError)
+                               return
+                       }
+                       tag["icon"] = decryptedIcon
+               }
+
+               if encName, ok := tag["name"].(string); ok {
+                       decryptedName, err := utils.DecryptText(encName, encKey)
+                       if err != nil {
+                               http.Error(w, fmt.Sprintf("Error decrypting tag name: %v", err), http.StatusInternalServerError)
+                               return
+                       }
+                       tag["name"] = decryptedName
+               }
+
+               if encColor, ok := tag["color"].(string); ok {
+                       decryptedColor, err := utils.DecryptText(encColor, encKey)
+                       if err != nil {
+                               http.Error(w, fmt.Sprintf("Error decrypting tag color: %v", err), http.StatusInternalServerError)
+                               return
+                       }
+                       tag["color"] = decryptedColor
+               }
+
+               result = append(result, tag)
+       }
+
+       // Return tags
+       utils.JSONResponse(w, http.StatusOK, result)
+}
+
+// TagRequest represents a tag request
+type TagRequest struct {
+       Icon  string `json:"icon"`
+       Name  string `json:"name"`
+       Color string `json:"color"`
+}
+
+// SaveTags handles saving a new tag
+func SaveTags(w http.ResponseWriter, r *http.Request) {
+       // Get user ID and derived key from context
+       userID, ok := r.Context().Value(utils.UserIDKey).(int)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+       derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+
+       // Parse request body
+       var req TagRequest
+       if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+               http.Error(w, "Invalid request body", http.StatusBadRequest)
+               return
+       }
+
+       // Get tags
+       content, err := utils.GetTags(userID)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error retrieving tags: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Create tags array if it doesn't exist
+       if _, ok := content["tags"]; !ok {
+               content["tags"] = []any{}
+       }
+       if _, ok := content["next_id"]; !ok {
+               content["next_id"] = 1
+       }
+
+       // Get encryption key
+       encKey, err := utils.GetEncryptionKey(userID, derivedKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error getting encryption key: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Check for duplicate tag names
+       tags, ok := content["tags"].([]any)
+       if ok {
+               for _, tagInterface := range tags {
+                       tag, ok := tagInterface.(map[string]any)
+                       if !ok {
+                               continue
+                       }
+
+                       if encName, ok := tag["name"].(string); ok {
+                               decryptedName, err := utils.DecryptText(encName, encKey)
+                               if err != nil {
+                                       http.Error(w, fmt.Sprintf("Error decrypting tag name: %v", err), http.StatusInternalServerError)
+                                       return
+                               }
+                               if decryptedName == req.Name {
+                                       http.Error(w, "Tag name already exists", http.StatusBadRequest)
+                                       return
+                               }
+                       }
+               }
+       }
+
+       // Encrypt tag data
+       encIcon, err := utils.EncryptText(req.Icon, encKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error encrypting tag icon: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       encName, err := utils.EncryptText(req.Name, encKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error encrypting tag name: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       encColor, err := utils.EncryptText(req.Color, encKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error encrypting tag color: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Create new tag
+       nextID, ok := content["next_id"].(float64)
+       if !ok {
+               nextID = 1
+       }
+
+       newTag := map[string]any{
+               "id":    int(nextID),
+               "icon":  encIcon,
+               "name":  encName,
+               "color": encColor,
+       }
+
+       // Add tag to tags array
+       tags, ok = content["tags"].([]any)
+       if !ok {
+               tags = []any{}
+       }
+       tags = append(tags, newTag)
+       content["tags"] = tags
+       content["next_id"] = nextID + 1
+
+       // Write tags
+       if err := utils.WriteTags(userID, content); err != nil {
+               http.Error(w, fmt.Sprintf("Error writing tags: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Return success
+       utils.JSONResponse(w, http.StatusOK, map[string]bool{
+               "success": true,
+       })
+}
diff --git a/backend/handlers/templates.go b/backend/handlers/templates.go
new file mode 100644 (file)
index 0000000..84ec826
--- /dev/null
@@ -0,0 +1,155 @@
+package handlers
+
+import (
+       "encoding/json"
+       "fmt"
+       "net/http"
+
+       "github.com/phitux/dailytxt/backend/utils"
+)
+
+// TemplatesRequest represents a templates request
+type TemplatesRequest struct {
+       Templates []struct {
+               Name string `json:"name"`
+               Text string `json:"text"`
+       } `json:"templates"`
+}
+
+// GetTemplates handles retrieving a user's templates
+func GetTemplates(w http.ResponseWriter, r *http.Request) {
+       // Get user ID and derived key from context
+       userID, ok := r.Context().Value(utils.UserIDKey).(int)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+       derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+
+       // Get templates
+       content, err := utils.GetTemplates(userID)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error retrieving templates: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // If no templates, return empty array
+       if templates, ok := content["templates"].([]any); !ok || len(templates) == 0 {
+               utils.JSONResponse(w, http.StatusOK, []any{})
+               return
+       }
+
+       // Get encryption key
+       encKey, err := utils.GetEncryptionKey(userID, derivedKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error getting encryption key: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Decrypt template data
+       templates := content["templates"].([]any)
+       result := []any{}
+
+       for _, templateInterface := range templates {
+               template, ok := templateInterface.(map[string]any)
+               if !ok {
+                       continue
+               }
+
+               // Decrypt name and text
+               if encName, ok := template["name"].(string); ok {
+                       decryptedName, err := utils.DecryptText(encName, encKey)
+                       if err != nil {
+                               http.Error(w, fmt.Sprintf("Error decrypting template name: %v", err), http.StatusInternalServerError)
+                               return
+                       }
+                       template["name"] = decryptedName
+               }
+
+               if encText, ok := template["text"].(string); ok {
+                       decryptedText, err := utils.DecryptText(encText, encKey)
+                       if err != nil {
+                               http.Error(w, fmt.Sprintf("Error decrypting template text: %v", err), http.StatusInternalServerError)
+                               return
+                       }
+                       template["text"] = decryptedText
+               }
+
+               result = append(result, template)
+       }
+
+       // Return templates
+       utils.JSONResponse(w, http.StatusOK, result)
+}
+
+// SaveTemplates handles saving templates
+func SaveTemplates(w http.ResponseWriter, r *http.Request) {
+       // Get user ID and derived key from context
+       userID, ok := r.Context().Value(utils.UserIDKey).(int)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+       derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+
+       // Parse request body
+       var req TemplatesRequest
+       if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+               http.Error(w, "Invalid request body", http.StatusBadRequest)
+               return
+       }
+
+       // Get encryption key
+       encKey, err := utils.GetEncryptionKey(userID, derivedKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error getting encryption key: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Create new templates content
+       content := map[string]any{
+               "templates": []any{},
+       }
+
+       // Encrypt template data
+       templates := []any{}
+       for _, template := range req.Templates {
+               encName, err := utils.EncryptText(template.Name, encKey)
+               if err != nil {
+                       http.Error(w, fmt.Sprintf("Error encrypting template name: %v", err), http.StatusInternalServerError)
+                       return
+               }
+
+               encText, err := utils.EncryptText(template.Text, encKey)
+               if err != nil {
+                       http.Error(w, fmt.Sprintf("Error encrypting template text: %v", err), http.StatusInternalServerError)
+                       return
+               }
+
+               templates = append(templates, map[string]any{
+                       "name": encName,
+                       "text": encText,
+               })
+       }
+
+       content["templates"] = templates
+
+       // Write templates
+       if err := utils.WriteTemplates(userID, content); err != nil {
+               http.Error(w, fmt.Sprintf("Error writing templates: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Return success
+       utils.JSONResponse(w, http.StatusOK, map[string]bool{
+               "success": true,
+       })
+}
index 540af3052f49d9f36c727b6e39f6e9d222a399dd..5684c8a99c87934f33856559d46f56c42e2de3e1 100644 (file)
@@ -5,7 +5,6 @@ import (
        "encoding/base64"
        "encoding/json"
        "fmt"
-       "maps"
        "net/http"
        "strings"
        "sync"
@@ -426,178 +425,6 @@ func CheckLogin(w http.ResponseWriter, r *http.Request) {
        })
 }
 
-func GetDefaultSettings() map[string]any {
-       // Default settings
-       return map[string]any{
-               "autoloadImagesByDefault":    false,
-               "setAutoloadImagesPerDevice": true,
-               "useALookBack":               true,
-               "aLookBackYears":             []int{1, 5, 10},
-               "useBrowserTimezone":         true,
-               "timezone":                   "UTC",
-               "useBrowserLanguage":         true,
-               "language":                   "en",
-               "darkModeAutoDetect":         true,
-               "useDarkMode":                false,
-               "background":                 "gradient",
-               "monochromeBackgroundColor":  "#ececec",
-               "checkForUpdates":            true,
-               "includeTestVersions":        false,
-               "requirePasswordOnPageLoad":  false,
-       }
-}
-
-// GetUserSettings retrieves user settings
-func GetUserSettings(w http.ResponseWriter, r *http.Request) {
-       // Get user ID from context
-       userID, ok := r.Context().Value(utils.UserIDKey).(int)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
-               return
-       }
-
-       // Get derived key from context
-       derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
-               return
-       }
-
-       // Get user settings
-       encryptedSettings, err := utils.GetUserSettings(userID)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error retrieving user settings: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Default settings
-       defaultSettings := GetDefaultSettings()
-
-       // If no settings found, return defaults
-       if len(encryptedSettings) == 0 {
-               utils.JSONResponse(w, http.StatusOK, defaultSettings)
-               return
-       }
-
-       // Decrypt settings
-       encKey, err := utils.GetEncryptionKey(userID, derivedKey)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error getting encryption key: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       decryptedSettings, err := utils.DecryptText(encryptedSettings, encKey)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error decrypting settings: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Parse JSON
-       var settings map[string]any
-       if err := json.Unmarshal([]byte(decryptedSettings), &settings); err != nil {
-               http.Error(w, fmt.Sprintf("Error parsing settings: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Apply defaults for missing keys
-       for key, value := range defaultSettings {
-               if _, exists := settings[key]; !exists {
-                       settings[key] = value
-               }
-       }
-
-       // Return settings
-       utils.JSONResponse(w, http.StatusOK, settings)
-}
-
-// SaveUserSettings saves user settings
-func SaveUserSettings(w http.ResponseWriter, r *http.Request) {
-       // Get user ID from context
-       userID, ok := r.Context().Value(utils.UserIDKey).(int)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
-               return
-       }
-
-       // Get derived key from context
-       derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
-       if !ok {
-               http.Error(w, "Unauthorized", http.StatusUnauthorized)
-               return
-       }
-
-       // Parse request body
-       var newSettings map[string]any
-       if err := json.NewDecoder(r.Body).Decode(&newSettings); err != nil {
-               http.Error(w, "Invalid request body", http.StatusBadRequest)
-               return
-       }
-
-       // Get existing settings
-       encryptedSettings, err := utils.GetUserSettings(userID)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error retrieving user settings: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Get encryption key
-       encKey, err := utils.GetEncryptionKey(userID, derivedKey)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error getting encryption key: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Current settings
-       var currentSettings map[string]any
-
-       // If settings exist, decrypt them
-       if len(encryptedSettings) > 0 {
-               decryptedSettings, err := utils.DecryptText(encryptedSettings, encKey)
-               if err != nil {
-                       http.Error(w, fmt.Sprintf("Error decrypting settings: %v", err), http.StatusInternalServerError)
-                       return
-               }
-
-               // Parse JSON
-               if err := json.Unmarshal([]byte(decryptedSettings), &currentSettings); err != nil {
-                       http.Error(w, fmt.Sprintf("Error parsing settings: %v", err), http.StatusInternalServerError)
-                       return
-               }
-       }
-
-       // If no settings or empty, use defaults
-       if len(currentSettings) == 0 {
-               currentSettings = GetDefaultSettings()
-       }
-
-       // Update settings
-       maps.Copy(currentSettings, newSettings)
-
-       // Encrypt settings
-       settingsJSON, err := json.Marshal(currentSettings)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error encoding settings: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       encryptedNewSettings, err := utils.EncryptText(string(settingsJSON), encKey)
-       if err != nil {
-               http.Error(w, fmt.Sprintf("Error encrypting settings: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Write settings
-       if err := utils.WriteUserSettings(userID, encryptedNewSettings); err != nil {
-               http.Error(w, fmt.Sprintf("Error writing settings: %v", err), http.StatusInternalServerError)
-               return
-       }
-
-       // Return success
-       utils.JSONResponse(w, http.StatusOK, map[string]bool{
-               "success": true,
-       })
-}
-
 // MigrationProgress stores the progress of user data migration
 type MigrationProgress struct {
        Phase          string `json:"phase"`           // Current migration phase
git clone https://git.99rst.org/PROJECT