From: PhiTux Date: Mon, 6 Oct 2025 19:33:30 +0000 (+0200) Subject: rearranged go methods+files X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=c79291641fe8e7d13463c16202c386d1f905eb96;p=DailyTxT.git rearranged go methods+files --- diff --git a/backend/handlers/admin.go b/backend/handlers/admin.go index e62cfa3..897b6ba 100644 --- a/backend/handlers/admin.go +++ b/backend/handlers/admin.go @@ -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 index 0000000..95f409a --- /dev/null +++ b/backend/handlers/files.go @@ -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}) +} diff --git a/backend/handlers/logs.go b/backend/handlers/logs.go index faf59af..764a7cf 100644 --- a/backend/handlers/logs.go +++ b/backend/handlers/logs.go @@ -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}) -} diff --git a/backend/handlers/additional.go b/backend/handlers/search.go similarity index 64% rename from backend/handlers/additional.go rename to backend/handlers/search.go index d2d9d4d..b1a550b 100644 --- a/backend/handlers/additional.go +++ b/backend/handlers/search.go @@ -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 index 0000000..6aa3ebe --- /dev/null +++ b/backend/handlers/settings.go @@ -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), ¤tSettings); 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 index 0000000..25b849f --- /dev/null +++ b/backend/handlers/tags.go @@ -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 index 0000000..84ec826 --- /dev/null +++ b/backend/handlers/templates.go @@ -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, + }) +} diff --git a/backend/handlers/users.go b/backend/handlers/users.go index 540af30..5684c8a 100644 --- a/backend/handlers/users.go +++ b/backend/handlers/users.go @@ -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), ¤tSettings); 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