added go backend
authorPhiTux <redacted>
Wed, 9 Jul 2025 17:47:09 +0000 (19:47 +0200)
committerPhiTux <redacted>
Wed, 9 Jul 2025 17:47:09 +0000 (19:47 +0200)
23 files changed:
backend-python/.gitignore [moved from backend/.gitignore with 100% similarity]
backend-python/requirements.txt [moved from backend/requirements.txt with 100% similarity]
backend-python/server/__init__.py [moved from backend/server/__init__.py with 100% similarity]
backend-python/server/main.py [moved from backend/server/main.py with 100% similarity]
backend-python/server/routers/__init__.py [moved from backend/server/routers/__init__.py with 100% similarity]
backend-python/server/routers/logs.py [moved from backend/server/routers/logs.py with 100% similarity]
backend-python/server/routers/users.py [moved from backend/server/routers/users.py with 100% similarity]
backend-python/server/utils/fileHandling.py [moved from backend/server/utils/fileHandling.py with 100% similarity]
backend-python/server/utils/security.py [moved from backend/server/utils/security.py with 100% similarity]
backend-python/server/utils/settings.py [moved from backend/server/utils/settings.py with 100% similarity]
backend/.envrc [new file with mode: 0644]
backend/backend [new file with mode: 0755]
backend/data/1/2025/07.json [new file with mode: 0644]
backend/data/users.json [new file with mode: 0644]
backend/go.mod [new file with mode: 0644]
backend/go.sum [new file with mode: 0644]
backend/handlers/additional.go [new file with mode: 0644]
backend/handlers/logs.go [new file with mode: 0644]
backend/handlers/users.go [new file with mode: 0644]
backend/main.go [new file with mode: 0644]
backend/middleware/middleware.go [new file with mode: 0644]
backend/utils/file_handling.go [new file with mode: 0644]
backend/utils/security.go [new file with mode: 0644]

similarity index 100%
rename from backend/.gitignore
rename to backend-python/.gitignore
diff --git a/backend/.envrc b/backend/.envrc
new file mode 100644 (file)
index 0000000..67689f2
--- /dev/null
@@ -0,0 +1,7 @@
+#export VIRTUAL_ENV=."venv"
+#layout python python3.12
+export DATA_PATH=~/git/DailyTxT/backend/data
+export SECRET_TOKEN=secret
+export ALLOWED_HOSTS='["http://localhost:5173","http://127.0.0.1:5173","http://lab:5173"]'
+export INDENT=4
+export DEVELOPMENT=true
diff --git a/backend/backend b/backend/backend
new file mode 100755 (executable)
index 0000000..e3261cb
Binary files /dev/null and b/backend/backend differ
diff --git a/backend/data/1/2025/07.json b/backend/data/1/2025/07.json
new file mode 100644 (file)
index 0000000..31e36ad
--- /dev/null
@@ -0,0 +1,26 @@
+{
+    "days": [
+        {
+            "date_written": "PQKE7k8vv4ywUxaSNmPTWELeu6AWlK7tCfVvmz9zAurW1n5Z1ikH0a82c3Cg",
+            "day": 9,
+            "history": [
+                {
+                    "date_written": "W4jLc1ViEbYH0VJkIcny2M8zIpK8dLqs_hSkM20Xh1FZR5sNrfRBMM67X0yl",
+                    "text": "zFodQaGCYCSFznwtPpGOlp5Vm891UdhkWHC0RLIBnCM=",
+                    "version": 1
+                },
+                {
+                    "date_written": "0eQZARHYhhPgyJsMhyrUhrzBClx6_6ecnSQFCXLwJZJLz9sw4_Z6GM5olUtF",
+                    "text": "q0maYDSZh1fxlJEsKm86IfJIPeeAj9lzg6uLohUyRfc8",
+                    "version": 2
+                },
+                {
+                    "date_written": "_tHe4SzxePVShjhgI56FOanKI-NzzH0ULLoX7aiEsFU3QRkwdVfgy8LVsnPk",
+                    "text": "jxexvwudf9VtmEecZumS3CX-fJYb7QBNUYfpf651UaPT",
+                    "version": 3
+                }
+            ],
+            "text": "ixfArbVLC4AFBvakTv4rPKqY1-QR5UU0hTQqQA25Emc="
+        }
+    ]
+}
diff --git a/backend/data/users.json b/backend/data/users.json
new file mode 100644 (file)
index 0000000..d0ed21e
--- /dev/null
@@ -0,0 +1 @@
+{"id_counter":1,"users":[{"dailytxt_version":2,"enc_enc_key":"F72a95Ro/OP1Oa0dwNtbFUrpw9o7f1X3P73fNwj+ZzYqPGj8j94D48ewYc+CZ4Db3NrtBipVRKOXrKyq","password":"SgmHrbBGbhaQ4ibSq4GNcD3PES46kg4pB0xqBZdAjB0=","salt":"dj9yX4A0r6U6zkG7Ln4LhA==","user_id":1,"username":"username"}]}
diff --git a/backend/go.mod b/backend/go.mod
new file mode 100644 (file)
index 0000000..d2211d1
--- /dev/null
@@ -0,0 +1,10 @@
+module github.com/phitux/dailytxt/backend
+
+go 1.22
+
+require (
+       github.com/golang-jwt/jwt/v5 v5.2.0
+       golang.org/x/crypto v0.19.0
+)
+
+require golang.org/x/sys v0.17.0 // indirect
diff --git a/backend/go.sum b/backend/go.sum
new file mode 100644 (file)
index 0000000..b22b2a2
--- /dev/null
@@ -0,0 +1,6 @@
+github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
+github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
diff --git a/backend/handlers/additional.go b/backend/handlers/additional.go
new file mode 100644 (file)
index 0000000..42da002
--- /dev/null
@@ -0,0 +1,1327 @@
+package handlers
+
+import (
+       "encoding/json"
+       "fmt"
+       "io"
+       "net/http"
+       "strconv"
+       "strings"
+
+       "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,
+       })
+}
+
+// LoadMonthForReading handles loading a month for reading
+func LoadMonthForReading(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 parameters from URL
+       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 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 if days exist
+       days, ok := content["days"].([]any)
+       if !ok {
+               utils.JSONResponse(w, http.StatusOK, []any{})
+               return
+       }
+
+       // Process days
+       result := []any{}
+       for _, dayInterface := range days {
+               day, ok := dayInterface.(map[string]any)
+               if !ok {
+                       continue
+               }
+
+               dayNum, ok := day["day"].(float64)
+               if !ok {
+                       continue
+               }
+
+               // Create result day
+               resultDay := map[string]any{
+                       "day": int(dayNum),
+               }
+
+               // Decrypt text and date_written
+               if text, ok := day["text"].(string); ok && text != "" {
+                       decryptedText, err := utils.DecryptText(text, encKey)
+                       if err != nil {
+                               http.Error(w, fmt.Sprintf("Error decrypting text: %v", err), http.StatusInternalServerError)
+                               return
+                       }
+                       resultDay["text"] = decryptedText
+
+                       if dateWritten, ok := day["date_written"].(string); ok && dateWritten != "" {
+                               decryptedDate, err := utils.DecryptText(dateWritten, encKey)
+                               if err != nil {
+                                       http.Error(w, fmt.Sprintf("Error decrypting date_written: %v", err), http.StatusInternalServerError)
+                                       return
+                               }
+                               resultDay["date_written"] = decryptedDate
+                       }
+               }
+
+               // Get tags
+               if tags, ok := day["tags"].([]any); ok && len(tags) > 0 {
+                       resultDay["tags"] = tags
+               }
+
+               // Decrypt filenames if files exist
+               if filesList, ok := day["files"].([]any); ok && len(filesList) > 0 {
+                       files := []any{}
+                       for _, fileInterface := range filesList {
+                               file, ok := fileInterface.(map[string]any)
+                               if !ok {
+                                       continue
+                               }
+
+                               if encFilename, ok := file["enc_filename"].(string); ok {
+                                       decryptedFilename, err := utils.DecryptText(encFilename, encKey)
+                                       if err != nil {
+                                               http.Error(w, fmt.Sprintf("Error decrypting filename: %v", err), http.StatusInternalServerError)
+                                               return
+                                       }
+                                       fileCopy := make(map[string]any)
+                                       for k, v := range file {
+                                               fileCopy[k] = v
+                                       }
+                                       fileCopy["filename"] = decryptedFilename
+                                       files = append(files, fileCopy)
+                               }
+                       }
+                       resultDay["files"] = files
+               }
+
+               // Add day to result if it has content
+               if _, hasText := resultDay["text"]; hasText {
+                       result = append(result, resultDay)
+               } else if _, hasFiles := resultDay["files"]; hasFiles {
+                       result = append(result, resultDay)
+               } else if _, hasTags := resultDay["tags"]; hasTags {
+                       result = append(result, resultDay)
+               }
+       }
+
+       // Sort by day
+       /*
+               sort.Slice(result, func(i, j int) bool {
+                       dayI := result[i].(map[string]any)["day"].(int)
+                       dayJ := result[j].(map[string]any)["day"].(int)
+                       return dayI < dayJ
+               })
+       */
+
+       // Return result
+       utils.JSONResponse(w, http.StatusOK, result)
+}
+
+// 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()
+
+       // Read file
+       fileBytes, err := io.ReadAll(file)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error reading file: %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
+       }
+
+       // Encrypt file
+       encryptedFile, err := utils.EncryptFile(fileBytes, encKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error encrypting file: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Write file
+       if err := utils.WriteFile(encryptedFile, userID, uuid); err != nil {
+               http.Error(w, fmt.Sprintf("Error writing file: %v", err), http.StatusInternalServerError)
+               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
+       }
+
+       // 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
+       }
+
+       // Decrypt file
+       decryptedFile, err := utils.DecryptFile(encryptedFile, encKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error decrypting file: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // 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,
+       })
+}
+
+// GetHistory handles retrieving log history
+func GetHistory(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 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 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
+       }
+
+       // 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 {
+               utils.JSONResponse(w, http.StatusOK, []any{})
+               return
+       }
+
+       // Find day
+       for _, 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 history
+               history, ok := dayObj["history"].([]any)
+               if !ok || len(history) == 0 {
+                       utils.JSONResponse(w, http.StatusOK, []any{})
+                       return
+               }
+
+               // Decrypt history entries
+               result := []any{}
+               for _, historyInterface := range history {
+                       historyEntry, ok := historyInterface.(map[string]any)
+                       if !ok {
+                               continue
+                       }
+
+                       text, ok := historyEntry["text"].(string)
+                       if !ok {
+                               continue
+                       }
+
+                       dateWritten, ok := historyEntry["date_written"].(string)
+                       if !ok {
+                               continue
+                       }
+
+                       // Decrypt text and date
+                       decryptedText, err := utils.DecryptText(text, encKey)
+                       if err != nil {
+                               http.Error(w, fmt.Sprintf("Error decrypting history text: %v", err), http.StatusInternalServerError)
+                               return
+                       }
+
+                       decryptedDate, err := utils.DecryptText(dateWritten, encKey)
+                       if err != nil {
+                               http.Error(w, fmt.Sprintf("Error decrypting history date: %v", err), http.StatusInternalServerError)
+                               return
+                       }
+
+                       result = append(result, map[string]any{
+                               "text":         decryptedText,
+                               "date_written": decryptedDate,
+                       })
+               }
+
+               // Return history
+               utils.JSONResponse(w, http.StatusOK, result)
+               return
+       }
+
+       // Day not found
+       utils.JSONResponse(w, http.StatusOK, []any{})
+}
+
+// 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["bookmarked"].(bool); ok && bookmark {
+                       dayObj["bookmarked"] = false
+                       bookmarked = false
+               } else {
+                       dayObj["bookmarked"] = true
+               }
+               days[i] = dayObj
+               break
+       }
+
+       if !dayFound {
+               // Create new day with bookmark
+               days = append(days, map[string]any{
+                       "day":        day,
+                       "bookmarked": 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
+       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 parameters
+       tagIDStr := r.URL.Query().Get("tag_id")
+       if tagIDStr == "" {
+               http.Error(w, "Missing tag_id parameter", http.StatusBadRequest)
+               return
+       }
+       tagID, err := strconv.Atoi(tagIDStr)
+       if err != nil {
+               http.Error(w, "Invalid tag_id 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
+       }
+
+       // 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
+       }
+
+       // Search for tag
+       results := []any{}
+       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
+                       for _, dayInterface := range days {
+                               day, ok := dayInterface.(map[string]any)
+                               if !ok {
+                                       continue
+                               }
+
+                               dayNum, ok := day["day"].(float64)
+                               if !ok {
+                                       continue
+                               }
+
+                               tags, ok := day["tags"].([]any)
+                               if !ok {
+                                       continue
+                               }
+
+                               // Check if tag is in tags
+                               found := false
+                               for _, t := range tags {
+                                       if tagIDFloat, ok := t.(float64); ok && int(tagIDFloat) == tagID {
+                                               found = true
+                                               break
+                                       }
+                               }
+
+                               if !found {
+                                       continue
+                               }
+
+                               // Get text snippet
+                               context := ""
+                               if text, ok := day["text"].(string); ok && text != "" {
+                                       decryptedText, err := utils.DecryptText(text, encKey)
+                                       if err != nil {
+                                               continue
+                                       }
+                                       // Get first few words
+                                       words := strings.Fields(decryptedText)
+                                       if len(words) > 5 {
+                                               context = strings.Join(words[:5], " ")
+                                       } else {
+                                               context = decryptedText
+                                       }
+                               }
+
+                               // Add to results
+                               results = append(results, map[string]any{
+                                       "year":  yearInt,
+                                       "month": monthInt,
+                                       "day":   int(dayNum),
+                                       "text":  context,
+                               })
+                       }
+               }
+       }
+
+       // Sort results by date
+       /*
+               sort.Slice(results, func(i, j int) bool {
+                       ri := results[i].(map[string]any)
+                       rj := results[j].(map[string]any)
+
+                       yearI := ri["year"].(int)
+                       yearJ := rj["year"].(int)
+                       if yearI != yearJ {
+                               return yearI < yearJ
+                       }
+
+                       monthI := ri["month"].(int)
+                       monthJ := rj["month"].(int)
+                       if monthI != monthJ {
+                               return monthI < monthJ
+                       }
+
+                       dayI := ri["day"].(int)
+                       dayJ := rj["day"].(int)
+                       return dayI < dayJ
+               })
+       */
+
+       // Return results
+       utils.JSONResponse(w, http.StatusOK, results)
+}
diff --git a/backend/handlers/logs.go b/backend/handlers/logs.go
new file mode 100644 (file)
index 0000000..1eda899
--- /dev/null
@@ -0,0 +1,1105 @@
+package handlers
+
+import (
+       "encoding/json"
+       "fmt"
+       "html"
+       "net/http"
+       "os"
+       "path/filepath"
+       "regexp"
+       "sort"
+       "strconv"
+       "strings"
+
+       "github.com/phitux/dailytxt/backend/utils"
+)
+
+// LogRequest represents the log request body
+type LogRequest struct {
+       Day         int    `json:"day"`
+       Month       int    `json:"month"`
+       Year        int    `json:"year"`
+       Text        string `json:"text"`
+       DateWritten string `json:"date_written"`
+}
+
+// SaveLog handles saving a log entry
+func SaveLog(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 LogRequest
+       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 there's a previous log to move to history
+       historyAvailable := false
+       days, ok := content["days"].([]any)
+       if ok {
+               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
+                       }
+
+                       // If this day has text, move it to history
+                       if text, ok := day["text"].(string); ok && text != "" {
+                               historyAvailable = true
+                               historyVersion := 0
+
+                               // Get or create history array
+                               var history []any
+                               if historyArray, ok := day["history"].([]any); ok {
+                                       history = historyArray
+                                       // Find highest version
+                                       for _, historyItem := range history {
+                                               if historyMap, ok := historyItem.(map[string]any); ok {
+                                                       if version, ok := historyMap["version"].(float64); ok && int(version) > historyVersion {
+                                                               historyVersion = int(version)
+                                                       }
+                                               }
+                                       }
+                               } else {
+                                       history = []any{}
+                               }
+
+                               historyVersion++
+                               history = append(history, map[string]any{
+                                       "version":      historyVersion,
+                                       "text":         day["text"],
+                                       "date_written": day["date_written"],
+                               })
+
+                               day["history"] = history
+                               days[i] = day
+                       }
+                       break
+               }
+               content["days"] = days
+       }
+
+       // 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
+       }
+
+       // Encrypt text and date_written
+       encryptedText, err := utils.EncryptText(req.Text, encKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error encrypting text: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       encryptedDateWritten, err := utils.EncryptText(html.EscapeString(req.DateWritten), encKey)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error encrypting date_written: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Save new log
+       found := false
+       if days, ok := content["days"].([]any); ok {
+               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
+                       }
+
+                       // Update existing day
+                       day["text"] = encryptedText
+                       day["date_written"] = encryptedDateWritten
+                       days[i] = day
+                       found = true
+                       break
+               }
+
+               if !found {
+                       // Add new day
+                       days = append(days, map[string]any{
+                               "day":          req.Day,
+                               "text":         encryptedText,
+                               "date_written": encryptedDateWritten,
+                       })
+               }
+
+               content["days"] = days
+       } else {
+               // Create new days array
+               content["days"] = []any{
+                       map[string]any{
+                               "day":          req.Day,
+                               "text":         encryptedText,
+                               "date_written": encryptedDateWritten,
+                       },
+               }
+       }
+
+       // Write 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
+       }
+
+       // Return success
+       utils.JSONResponse(w, http.StatusOK, map[string]any{
+               "success":           true,
+               "history_available": historyAvailable,
+       })
+}
+
+// GetLog handles retrieving a log entry
+func GetLog(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 parameters from URL
+       year, err := strconv.Atoi(r.URL.Query().Get("year"))
+       if err != nil {
+               http.Error(w, "Invalid year parameter", http.StatusBadRequest)
+               return
+       }
+       month, err := strconv.Atoi(r.URL.Query().Get("month"))
+       if err != nil {
+               http.Error(w, "Invalid month parameter", http.StatusBadRequest)
+               return
+       }
+       dayValue, err := strconv.Atoi(r.URL.Query().Get("day"))
+       if err != nil {
+               http.Error(w, "Invalid day 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
+       }
+
+       // Default empty response
+       dummy := map[string]any{
+               "text":         "",
+               "date_written": "",
+               "files":        []any{},
+               "tags":         []any{},
+       }
+
+       // Check if days exist
+       days, ok := content["days"].([]any)
+       if !ok {
+               utils.JSONResponse(w, http.StatusOK, dummy)
+               return
+       }
+
+       // Find the day
+       for _, dayInterface := range days {
+               day, ok := dayInterface.(map[string]any)
+               if !ok {
+                       continue
+               }
+
+               dayNum, ok := day["day"].(float64)
+               if !ok || int(dayNum) != dayValue {
+                       continue
+               }
+
+               // 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 text and date_written
+               text := ""
+               dateWritten := ""
+               historyAvailable := false
+
+               if encryptedText, ok := day["text"].(string); ok && encryptedText != "" {
+                       decryptedText, err := utils.DecryptText(encryptedText, encKey)
+                       if err != nil {
+                               http.Error(w, fmt.Sprintf("Error decrypting text: %v", err), http.StatusInternalServerError)
+                               return
+                       }
+                       text = decryptedText
+               }
+
+               if encryptedDate, ok := day["date_written"].(string); ok && encryptedDate != "" {
+                       decryptedDate, err := utils.DecryptText(encryptedDate, encKey)
+                       if err != nil {
+                               http.Error(w, fmt.Sprintf("Error decrypting date_written: %v", err), http.StatusInternalServerError)
+                               return
+                       }
+                       dateWritten = decryptedDate
+               }
+
+               // Check for history
+               if history, ok := day["history"].([]any); ok && len(history) > 0 {
+                       historyAvailable = true
+               }
+
+               // Decrypt filenames if files exist
+               files := []any{}
+               if filesList, ok := day["files"].([]any); ok {
+                       for _, fileInterface := range filesList {
+                               file, ok := fileInterface.(map[string]any)
+                               if !ok {
+                                       continue
+                               }
+
+                               if encFilename, ok := file["enc_filename"].(string); ok {
+                                       decryptedFilename, err := utils.DecryptText(encFilename, encKey)
+                                       if err != nil {
+                                               http.Error(w, fmt.Sprintf("Error decrypting filename: %v", err), http.StatusInternalServerError)
+                                               return
+                                       }
+                                       fileCopy := make(map[string]any)
+                                       for k, v := range file {
+                                               fileCopy[k] = v
+                                       }
+                                       fileCopy["filename"] = decryptedFilename
+                                       files = append(files, fileCopy)
+                               }
+                       }
+               }
+
+               // Get tags
+               tags := []any{}
+               if tagsList, ok := day["tags"].([]any); ok {
+                       tags = tagsList
+               }
+
+               // Return log data
+               utils.JSONResponse(w, http.StatusOK, map[string]any{
+                       "text":              text,
+                       "date_written":      dateWritten,
+                       "files":             files,
+                       "tags":              tags,
+                       "history_available": historyAvailable,
+               })
+               return
+       }
+
+       // If day not found, return empty response
+       utils.JSONResponse(w, http.StatusOK, dummy)
+}
+
+// GetMarkedDays handles retrieving a month's logs
+func GetMarkedDays(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
+       }
+
+       // Get parameters from URL
+       year, err := strconv.Atoi(r.URL.Query().Get("year"))
+       if err != nil {
+               http.Error(w, "Invalid year parameter", http.StatusBadRequest)
+               return
+       }
+       month, err := strconv.Atoi(r.URL.Query().Get("month"))
+       if err != nil {
+               http.Error(w, "Invalid month 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
+       }
+
+       // Extract days with logs, files, and bookmarks
+       daysWithLogs := []int{}
+       daysWithFiles := []int{}
+       daysBookmarked := []int{}
+
+       if days, ok := content["days"].([]any); ok {
+               for _, dayInterface := range days {
+                       day, ok := dayInterface.(map[string]any)
+                       if !ok {
+                               continue
+                       }
+
+                       dayNum, ok := day["day"].(float64)
+                       if !ok {
+                               continue
+                       }
+
+                       // Check for text
+                       if _, ok := day["text"].(string); ok {
+                               daysWithLogs = append(daysWithLogs, int(dayNum))
+                       }
+
+                       // Check for files
+                       if files, ok := day["files"].([]any); ok && len(files) > 0 {
+                               daysWithFiles = append(daysWithFiles, int(dayNum))
+                       }
+
+                       // Check if bookmarked
+                       if bookmarked, ok := day["bookmarked"].(bool); ok && bookmarked {
+                               daysBookmarked = append(daysBookmarked, int(dayNum))
+                       }
+               }
+       }
+
+       // Return month data
+       utils.JSONResponse(w, http.StatusOK, map[string]any{
+               "days_with_logs":  daysWithLogs,
+               "days_with_files": daysWithFiles,
+               "days_bookmarked": daysBookmarked,
+       })
+}
+
+// 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
+       }
+
+       // 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,
+       })
+}
+
+// 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,
+       })
+}
+
+// GetOnThisDay handles retrieving logs from previous years on the same day
+func GetOnThisDay(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 parameters from URL
+       month, err := strconv.Atoi(r.PathValue("month"))
+       if err != nil {
+               http.Error(w, "Invalid month parameter", http.StatusBadRequest)
+               return
+       }
+       day, err := strconv.Atoi(r.PathValue("day"))
+       if err != nil {
+               http.Error(w, "Invalid day parameter", http.StatusBadRequest)
+               return
+       }
+
+       // Get query parameters
+       lastYears := r.URL.Query().Get("last_years")
+       if lastYears == "" {
+               http.Error(w, "Missing last_years parameter", http.StatusBadRequest)
+               return
+       }
+
+       // Parse years
+       yearStr := strings.Split(lastYears, ",")
+       years := []int{}
+       currentYear, err := strconv.Atoi(r.URL.Query().Get("year"))
+       if err != nil {
+               http.Error(w, "Invalid year parameter", http.StatusBadRequest)
+               return
+       }
+
+       for _, y := range yearStr {
+               if val, err := strconv.Atoi(y); err == nil {
+                       years = append(years, currentYear-val)
+               }
+       }
+
+       // 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
+       }
+
+       // Get logs from previous years
+       results := []any{}
+       for _, year := range years {
+               content, err := utils.GetMonth(userID, year, month)
+               if err != nil {
+                       continue
+               }
+
+               days, ok := content["days"].([]any)
+               if !ok {
+                       continue
+               }
+
+               for _, dayInterface := range days {
+                       dayLog, ok := dayInterface.(map[string]any)
+                       if !ok {
+                               continue
+                       }
+
+                       dayNum, ok := dayLog["day"].(float64)
+                       if !ok || int(dayNum) != day {
+                               continue
+                       }
+
+                       text, ok := dayLog["text"].(string)
+                       if !ok || text == "" {
+                               continue
+                       }
+
+                       // Decrypt text
+                       decryptedText, err := utils.DecryptText(text, encKey)
+                       if err != nil {
+                               continue
+                       }
+
+                       results = append(results, map[string]any{
+                               "years_old": currentYear - year,
+                               "day":       day,
+                               "month":     month,
+                               "year":      year,
+                               "text":      decryptedText,
+                       })
+                       break
+               }
+       }
+
+       // Return results
+       utils.JSONResponse(w, http.StatusOK, results)
+}
+
+// Helper functions for search
+func getStartIndex(text string, index int) int {
+       if index == 0 {
+               return 0
+       }
+
+       for i := 0; i < 3; i++ {
+               startIndex := strings.LastIndex(text[:index-1], " ")
+               index = startIndex
+               if startIndex == -1 {
+                       return 0
+               }
+       }
+
+       return index + 1
+}
+
+func getEndIndex(text string, index int) int {
+       if index == len(text)-1 {
+               return len(text)
+       }
+
+       for i := 0; i < 3; i++ {
+               endIndex := strings.Index(text[index+1:], " ")
+               if endIndex == -1 {
+                       return len(text)
+               }
+               index = index + 1 + endIndex
+       }
+
+       return index
+}
+
+func getContext(text, searchString string, exact bool) string {
+       // Replace whitespace with non-breaking space
+       re := regexp.MustCompile(`\s+`)
+       text = re.ReplaceAllString(text, " ")
+
+       var pos int
+       if exact {
+               pos = strings.Index(text, searchString)
+       } else {
+               pos = strings.Index(strings.ToLower(text), strings.ToLower(searchString))
+       }
+
+       if pos == -1 {
+               return "<em>Dailytxt: Error formatting...</em>"
+       }
+
+       start := getStartIndex(text, pos)
+       end := getEndIndex(text, pos+len(searchString)-1)
+       return text[start:pos] + "<b>" + text[pos:pos+len(searchString)] + "</b>" + text[pos+len(searchString):end]
+}
+
+// Search handles searching logs for text
+func Search(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 query parameter
+       searchString := r.URL.Query().Get("q")
+       if searchString == "" {
+               http.Error(w, "Missing search 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
+       }
+
+       // Get user directory
+       userDir := filepath.Join(utils.Settings.DataPath, strconv.Itoa(userID))
+       results := []any{}
+
+       // Traverse all years and months
+       yearEntries, err := os.ReadDir(userDir)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error reading user directory: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Regex to match year directories (4 digits)
+       yearRegex := regexp.MustCompile(`^\d{4}$`)
+
+       for _, yearEntry := range yearEntries {
+               if !yearEntry.IsDir() || !yearRegex.MatchString(yearEntry.Name()) {
+                       continue
+               }
+               year := yearEntry.Name()
+
+               // Read month files in year directory
+               yearDir := filepath.Join(userDir, year)
+               monthEntries, err := os.ReadDir(yearDir)
+               if err != nil {
+                       continue
+               }
+
+               // Regex to match month files (2 digits + .json)
+               monthRegex := regexp.MustCompile(`^(\d{2})\.json$`)
+
+               for _, monthEntry := range monthEntries {
+                       if monthEntry.IsDir() {
+                               continue
+                       }
+
+                       matches := monthRegex.FindStringSubmatch(monthEntry.Name())
+                       if len(matches) != 2 {
+                               continue
+                       }
+                       month := matches[1]
+
+                       // Get month content
+                       monthInt, _ := strconv.Atoi(month)
+                       yearInt, _ := strconv.Atoi(year)
+                       content, err := utils.GetMonth(userID, yearInt, monthInt)
+                       if err != nil {
+                               continue
+                       }
+
+                       days, ok := content["days"].([]any)
+                       if !ok {
+                               continue
+                       }
+
+                       // Process each day
+                       for _, dayInterface := range days {
+                               dayLog, ok := dayInterface.(map[string]any)
+                               if !ok {
+                                       continue
+                               }
+
+                               dayNum, ok := dayLog["day"].(float64)
+                               if !ok {
+                                       continue
+                               }
+                               day := int(dayNum)
+
+                               // Check text
+                               if text, ok := dayLog["text"].(string); ok {
+                                       decryptedText, err := utils.DecryptText(text, encKey)
+                                       if err != nil {
+                                               continue
+                                       }
+
+                                       // Apply search logic
+                                       if strings.HasPrefix(searchString, "\"") && strings.HasSuffix(searchString, "\"") {
+                                               // Exact match
+                                               searchTerm := searchString[1 : len(searchString)-1]
+                                               if strings.Contains(decryptedText, searchTerm) {
+                                                       context := getContext(decryptedText, searchTerm, true)
+                                                       results = append(results, map[string]any{
+                                                               "year":  year,
+                                                               "month": month,
+                                                               "day":   day,
+                                                               "text":  context,
+                                                       })
+                                               }
+                                       } else if strings.Contains(searchString, "|") {
+                                               // OR search
+                                               words := strings.Split(searchString, "|")
+                                               for _, word := range words {
+                                                       wordTrimmed := strings.TrimSpace(word)
+                                                       if strings.Contains(strings.ToLower(decryptedText), strings.ToLower(wordTrimmed)) {
+                                                               context := getContext(decryptedText, wordTrimmed, false)
+                                                               results = append(results, map[string]any{
+                                                                       "year":  year,
+                                                                       "month": month,
+                                                                       "day":   day,
+                                                                       "text":  context,
+                                                               })
+                                                               break
+                                                       }
+                                               }
+                                       } else if strings.Contains(searchString, " ") {
+                                               // AND search
+                                               words := strings.Split(searchString, " ")
+                                               allWordsMatch := true
+                                               for _, word := range words {
+                                                       wordTrimmed := strings.TrimSpace(word)
+                                                       if !strings.Contains(strings.ToLower(decryptedText), strings.ToLower(wordTrimmed)) {
+                                                               allWordsMatch = false
+                                                               break
+                                                       }
+                                               }
+                                               if allWordsMatch {
+                                                       context := getContext(decryptedText, strings.TrimSpace(words[0]), false)
+                                                       results = append(results, map[string]any{
+                                                               "year":  year,
+                                                               "month": month,
+                                                               "day":   day,
+                                                               "text":  context,
+                                                       })
+                                               }
+                                       } else {
+                                               // Simple search
+                                               if strings.Contains(strings.ToLower(decryptedText), strings.ToLower(searchString)) {
+                                                       context := getContext(decryptedText, searchString, false)
+                                                       results = append(results, map[string]any{
+                                                               "year":  year,
+                                                               "month": month,
+                                                               "day":   day,
+                                                               "text":  context,
+                                                       })
+                                               }
+                                       }
+                               }
+
+                               // Check filenames
+                               if files, ok := dayLog["files"].([]any); ok {
+                                       for _, fileInterface := range files {
+                                               file, ok := fileInterface.(map[string]any)
+                                               if !ok {
+                                                       continue
+                                               }
+
+                                               if encFilename, ok := file["enc_filename"].(string); ok {
+                                                       decryptedFilename, err := utils.DecryptText(encFilename, encKey)
+                                                       if err != nil {
+                                                               continue
+                                                       }
+
+                                                       if strings.Contains(strings.ToLower(decryptedFilename), strings.ToLower(searchString)) {
+                                                               context := "📎 " + decryptedFilename
+                                                               results = append(results, map[string]any{
+                                                                       "year":  year,
+                                                                       "month": month,
+                                                                       "day":   day,
+                                                                       "text":  context,
+                                                               })
+                                                               break
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+               }
+       }
+
+       // Sort results by date
+       sort.Slice(results, func(i, j int) bool {
+               ri := results[i].(map[string]any)
+               rj := results[j].(map[string]any)
+
+               yearI, _ := strconv.Atoi(ri["year"].(string))
+               yearJ, _ := strconv.Atoi(rj["year"].(string))
+               if yearI != yearJ {
+                       return yearI < yearJ
+               }
+
+               monthI, _ := strconv.Atoi(ri["month"].(string))
+               monthJ, _ := strconv.Atoi(rj["month"].(string))
+               if monthI != monthJ {
+                       return monthI < monthJ
+               }
+
+               dayI := ri["day"].(int)
+               dayJ := rj["day"].(int)
+               return dayI < dayJ
+       })
+
+       // Return results
+       utils.JSONResponse(w, http.StatusOK, results)
+}
diff --git a/backend/handlers/users.go b/backend/handlers/users.go
new file mode 100644 (file)
index 0000000..448e454
--- /dev/null
@@ -0,0 +1,457 @@
+package handlers
+
+import (
+       "encoding/base64"
+       "encoding/json"
+       "fmt"
+       "net/http"
+       "time"
+
+       "github.com/phitux/dailytxt/backend/utils"
+)
+
+// LoginRequest represents the login request body
+type LoginRequest struct {
+       Username string `json:"username"`
+       Password string `json:"password"`
+}
+
+// Login handles user login
+func Login(w http.ResponseWriter, r *http.Request) {
+       // Parse the request body
+       var req LoginRequest
+       if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+               http.Error(w, "Invalid request body", http.StatusBadRequest)
+               return
+       }
+
+       // Get users
+       users, err := utils.GetUsers()
+       if err != nil {
+               http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+               return
+       }
+
+       // Check if user exists
+       usersList, ok := users["users"].([]any)
+       if !ok || len(usersList) == 0 {
+               utils.Logger.Printf("Login failed. User '%s' not found", req.Username)
+               http.Error(w, "User/Password combination not found", http.StatusNotFound)
+               return
+       }
+
+       // Find user
+       var userID int
+       var hashedPassword string
+       var salt string
+       found := false
+
+       for _, u := range usersList {
+               user, ok := u.(map[string]any)
+               if !ok {
+                       continue
+               }
+
+               if username, ok := user["username"].(string); ok && username == req.Username {
+                       found = true
+                       if id, ok := user["user_id"].(float64); ok {
+                               userID = int(id)
+                       }
+                       if pwd, ok := user["password"].(string); ok {
+                               hashedPassword = pwd
+                       }
+                       if s, ok := user["salt"].(string); ok {
+                               salt = s
+                       }
+                       break
+               }
+       }
+
+       if !found {
+               utils.Logger.Printf("Login failed. User '%s' not found", req.Username)
+               http.Error(w, "User/Password combination not found", http.StatusNotFound)
+               return
+       }
+
+       // Verify password
+       if !utils.VerifyPassword(req.Password, hashedPassword, salt) {
+               utils.Logger.Printf("Login failed. Password for user '%s' is incorrect", req.Username)
+               http.Error(w, "User/Password combination not found", http.StatusNotFound)
+               return
+       }
+
+       // Get intermediate key
+       derivedKey, err := utils.DeriveKeyFromPassword(req.Password, salt)
+       if err != nil {
+               http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+               return
+       }
+       derivedKeyBase64 := base64.StdEncoding.EncodeToString(derivedKey)
+
+       // Create JWT token
+       token, err := utils.GenerateToken(userID, req.Username, derivedKeyBase64)
+       if err != nil {
+               http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+               return
+       }
+
+       // Set cookie
+       http.SetCookie(w, &http.Cookie{
+               Name:     "token",
+               Value:    token,
+               HttpOnly: true,
+               SameSite: http.SameSiteLaxMode,
+               Path:     "/",
+               Expires:  time.Now().Add(time.Duration(utils.Settings.LogoutAfterDays) * 24 * time.Hour),
+       })
+
+       // Return success
+       utils.JSONResponse(w, http.StatusOK, map[string]string{
+               "username": req.Username,
+       })
+}
+
+// RegisterRequest represents the register request body
+type RegisterRequest struct {
+       Username string `json:"username"`
+       Password string `json:"password"`
+}
+
+// Register handles user registration
+func Register(w http.ResponseWriter, r *http.Request) {
+       // Parse the request body
+       var req RegisterRequest
+       if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+               http.Error(w, "Invalid request body", http.StatusBadRequest)
+               return
+       }
+
+       // Get users
+       users, err := utils.GetUsers()
+       if err != nil {
+               http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+               return
+       }
+
+       // Check if username already exists
+       if len(users) > 0 {
+               usersList, ok := users["users"].([]any)
+               if !ok {
+                       utils.Logger.Printf("users.json is not in the correct format. Key 'users' is missing or not a list.")
+                       http.Error(w, "users.json is not in the correct format", http.StatusInternalServerError)
+                       return
+               }
+
+               for _, u := range usersList {
+                       user, ok := u.(map[string]any)
+                       if !ok {
+                               continue
+                       }
+
+                       if username, ok := user["username"].(string); ok && username == req.Username {
+                               utils.Logger.Printf("Registration failed. Username '%s' already exists", req.Username)
+                               http.Error(w, "Username already exists", http.StatusBadRequest)
+                               return
+                       }
+               }
+       }
+
+       // Create new user data
+       hashedPassword, salt, err := utils.HashPassword(req.Password)
+       if err != nil {
+               http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+               return
+       }
+
+       // Create encryption key
+       derivedKey, err := utils.DeriveKeyFromPassword(req.Password, salt)
+       if err != nil {
+               http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+               return
+       }
+
+       // Generate a new random encryption key
+       encryptionKey := make([]byte, 32)
+       if _, err := utils.RandRead(encryptionKey); err != nil {
+               http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+               return
+       }
+
+       // Encrypt the encryption key with the derived key
+       aead, err := utils.CreateAEAD(derivedKey)
+       if err != nil {
+               http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+               return
+       }
+
+       nonce := make([]byte, aead.NonceSize())
+       if _, err := utils.RandRead(nonce); err != nil {
+               http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+               return
+       }
+
+       encryptedKey := aead.Seal(nonce, nonce, encryptionKey, nil)
+       encEncKey := base64.StdEncoding.EncodeToString(encryptedKey)
+
+       // Create or update users
+       if len(users) == 0 {
+               users = map[string]any{
+                       "id_counter": 1,
+                       "users": []map[string]any{
+                               {
+                                       "user_id":          1,
+                                       "dailytxt_version": 2,
+                                       "username":         req.Username,
+                                       "password":         hashedPassword,
+                                       "salt":             salt,
+                                       "enc_enc_key":      encEncKey,
+                               },
+                       },
+               }
+       } else {
+               // Increment ID counter
+               idCounter, ok := users["id_counter"].(float64)
+               if !ok {
+                       idCounter = 1
+               }
+               idCounter++
+               users["id_counter"] = idCounter
+
+               // Add new user
+               usersList, ok := users["users"].([]any)
+               if !ok {
+                       usersList = []any{}
+               }
+
+               usersList = append(usersList, map[string]any{
+                       "user_id":          int(idCounter),
+                       "dailytxt_version": 2,
+                       "username":         req.Username,
+                       "password":         hashedPassword,
+                       "salt":             salt,
+                       "enc_enc_key":      encEncKey,
+               })
+
+               users["users"] = usersList
+       }
+
+       // Write users to file
+       if err := utils.WriteUsers(users); err != nil {
+               http.Error(w, "Internal Server Error when trying to write users.json", http.StatusInternalServerError)
+               return
+       }
+
+       // Return success
+       utils.JSONResponse(w, http.StatusOK, map[string]bool{
+               "success": true,
+       })
+}
+
+// Logout handles user logout
+func Logout(w http.ResponseWriter, r *http.Request) {
+       // Delete token cookie
+       http.SetCookie(w, &http.Cookie{
+               Name:     "token",
+               Value:    "",
+               HttpOnly: true,
+               SameSite: http.SameSiteLaxMode,
+               Path:     "/",
+               Expires:  time.Unix(0, 0),
+               MaxAge:   -1,
+       })
+
+       // Return success
+       utils.JSONResponse(w, http.StatusOK, map[string]bool{
+               "success": true,
+       })
+}
+
+// CheckLogin checks if user is logged in
+func CheckLogin(w http.ResponseWriter, r *http.Request) {
+       // Get token from cookie
+       cookie, err := r.Cookie("token")
+       if err != nil {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+
+       // Validate JWT token
+       claims, err := utils.ValidateToken(cookie.Value)
+       if err != nil {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+
+       // Return user info
+       utils.JSONResponse(w, http.StatusOK, map[string]any{
+               "user_id":  claims.UserID,
+               "username": claims.Username,
+       })
+}
+
+// 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 := map[string]any{
+               "autoloadImagesByDefault":    false,
+               "setAutoloadImagesPerDevice": true,
+               "useOnThisDay":               true,
+               "onThisDayYears":             []int{1, 5, 10},
+               "useBrowserTimezone":         true,
+               "timezone":                   "UTC",
+       }
+
+       // 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 = map[string]any{
+                       "autoloadImagesByDefault":    false,
+                       "setAutoloadImagesPerDevice": true,
+                       "useOnThisDay":               true,
+                       "onThisDayYears":             []int{1, 5, 10},
+                       "useBrowserTimezone":         true,
+                       "timezone":                   "UTC",
+               }
+       }
+
+       // Update settings
+       for key, value := range newSettings {
+               currentSettings[key] = value
+       }
+
+       // 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/main.go b/backend/main.go
new file mode 100644 (file)
index 0000000..d9c13ba
--- /dev/null
@@ -0,0 +1,95 @@
+package main
+
+import (
+       "context"
+       "log"
+       "net/http"
+       "os"
+       "os/signal"
+       "syscall"
+       "time"
+
+       "github.com/phitux/dailytxt/backend/handlers"
+       "github.com/phitux/dailytxt/backend/middleware"
+       "github.com/phitux/dailytxt/backend/utils"
+)
+
+func main() {
+       // Setup logging
+       logger := log.New(os.Stdout, "dailytxt: ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
+       logger.Println("Server starting...")
+
+       // Load settings
+       if err := utils.InitSettings(); err != nil {
+               logger.Fatalf("Failed to initialize settings: %v", err)
+       }
+
+       // Create a new router
+       mux := http.NewServeMux()
+
+       // Register routes
+       mux.HandleFunc("POST /users/login", handlers.Login)
+       mux.HandleFunc("POST /users/register", handlers.Register)
+       mux.HandleFunc("GET /users/logout", handlers.Logout)
+       mux.HandleFunc("GET /users/check", middleware.RequireAuth(handlers.CheckLogin))
+       mux.HandleFunc("GET /users/getUserSettings", middleware.RequireAuth(handlers.GetUserSettings))
+       mux.HandleFunc("POST /users/saveUserSettings", middleware.RequireAuth(handlers.SaveUserSettings))
+
+       mux.HandleFunc("POST /logs/saveLog", middleware.RequireAuth(handlers.SaveLog))
+       mux.HandleFunc("GET /logs/getLog", middleware.RequireAuth(handlers.GetLog))
+       mux.HandleFunc("GET /logs/getMarkedDays", middleware.RequireAuth(handlers.GetMarkedDays))
+       mux.HandleFunc("GET /logs/getTags", middleware.RequireAuth(handlers.GetTags))
+       mux.HandleFunc("POST /logs/saveNewTag", middleware.RequireAuth(handlers.SaveTags))
+       mux.HandleFunc("POST /logs/editTag", middleware.RequireAuth(handlers.EditTag))
+       mux.HandleFunc("GET /logs/deleteTag", middleware.RequireAuth(handlers.DeleteTag))
+       mux.HandleFunc("POST /logs/addTagToLog", middleware.RequireAuth(handlers.AddTagToLog))
+       mux.HandleFunc("POST /logs/removeTagFromLog", middleware.RequireAuth(handlers.RemoveTagFromLog))
+       mux.HandleFunc("GET /logs/getTemplates", middleware.RequireAuth(handlers.GetTemplates))
+       mux.HandleFunc("POST /logs/saveTemplates", middleware.RequireAuth(handlers.SaveTemplates))
+       mux.HandleFunc("GET /logs/getOnThisDay", middleware.RequireAuth(handlers.GetOnThisDay))
+       mux.HandleFunc("GET /logs/searchString", middleware.RequireAuth(handlers.Search))
+       mux.HandleFunc("GET /logs/searchTag", middleware.RequireAuth(handlers.SearchTag))
+       mux.HandleFunc("GET /logs/loadMonthForReading", middleware.RequireAuth(handlers.LoadMonthForReading))
+       mux.HandleFunc("POST /logs/uploadFile", middleware.RequireAuth(handlers.UploadFile))
+       mux.HandleFunc("GET /logs/downloadFile", middleware.RequireAuth(handlers.DownloadFile))
+       mux.HandleFunc("GET /logs/deleteFile", middleware.RequireAuth(handlers.DeleteFile))
+       mux.HandleFunc("GET /logs/getHistory", middleware.RequireAuth(handlers.GetHistory))
+       mux.HandleFunc("GET /logs/bookmarkDay", middleware.RequireAuth(handlers.BookmarkDay))
+
+       // Create a handler with CORS middleware
+       handler := middleware.CORS(mux)
+
+       // Create the server
+       server := &http.Server{
+               Addr:         ":8000",
+               Handler:      handler,
+               ReadTimeout:  15 * time.Second,
+               WriteTimeout: 15 * time.Second,
+               IdleTimeout:  60 * time.Second,
+       }
+
+       // Start the server in a goroutine
+       go func() {
+               logger.Println("Server listening on :8000")
+               if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+                       logger.Fatalf("Failed to start server: %v", err)
+               }
+       }()
+
+       // Setup graceful shutdown
+       quit := make(chan os.Signal, 1)
+       signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
+       <-quit
+       logger.Println("Shutting down server...")
+
+       // Create a deadline to wait for
+       ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+       defer cancel()
+
+       // Doesn't block if no connections, otherwise wait until the timeout
+       if err := server.Shutdown(ctx); err != nil {
+               logger.Fatalf("Server forced to shutdown: %v", err)
+       }
+
+       logger.Println("Server stopped gracefully")
+}
diff --git a/backend/middleware/middleware.go b/backend/middleware/middleware.go
new file mode 100644 (file)
index 0000000..a8721df
--- /dev/null
@@ -0,0 +1,88 @@
+package middleware
+
+import (
+       "context"
+       "net/http"
+       "strings"
+
+       "github.com/phitux/dailytxt/backend/utils"
+)
+
+// CORS middleware handles Cross-Origin Resource Sharing
+func CORS(next http.Handler) http.Handler {
+       return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+               // Get origin from request
+               origin := r.Header.Get("Origin")
+
+               // Check if origin is in allowed hosts
+               allowed := false
+               for _, host := range utils.Settings.AllowedHosts {
+                       if origin == host {
+                               allowed = true
+                               break
+                       }
+               }
+
+               // Set CORS headers if origin is allowed
+               if allowed {
+                       w.Header().Set("Access-Control-Allow-Origin", origin)
+                       w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
+                       w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
+                       w.Header().Set("Access-Control-Allow-Credentials", "true")
+               }
+
+               // Handle preflight requests
+               if r.Method == "OPTIONS" {
+                       w.WriteHeader(http.StatusOK)
+                       return
+               }
+
+               next.ServeHTTP(w, r)
+       })
+}
+
+// RequireAuth middleware checks if user is authenticated
+func RequireAuth(next http.HandlerFunc) http.HandlerFunc {
+       return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+               // Get token from cookie
+               cookie, err := r.Cookie("token")
+               if err != nil {
+                       http.Error(w, "Unauthorized", http.StatusUnauthorized)
+                       utils.Logger.Printf("Unauthorized access attempt, no cookie found: %s %s", r.Method, r.URL.Path)
+                       return
+               }
+
+               // Validate JWT token
+               claims, err := utils.ValidateToken(cookie.Value)
+               if err != nil {
+                       http.Error(w, "Unauthorized", http.StatusUnauthorized)
+                       utils.Logger.Printf("Unauthorized access attempt, invalid token: %s %s", r.Method, r.URL.Path)
+                       return
+               }
+
+               // Add user info to request context
+               ctx := context.WithValue(r.Context(), utils.UserIDKey, claims.UserID)
+               ctx = context.WithValue(ctx, utils.UsernameKey, claims.Username)
+               ctx = context.WithValue(ctx, utils.DerivedKeyKey, claims.DerivedKey)
+
+               // Continue with the next handler
+               next.ServeHTTP(w, r.WithContext(ctx))
+       })
+}
+
+// Logger middleware logs all requests
+func Logger(next http.Handler) http.Handler {
+       return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+               // Skip logging for static files
+               if strings.HasPrefix(r.URL.Path, "/static/") {
+                       next.ServeHTTP(w, r)
+                       return
+               }
+
+               // Log request
+               utils.Logger.Printf("Request: %s %s", r.Method, r.URL.Path)
+
+               // Continue with next handler
+               next.ServeHTTP(w, r)
+       })
+}
diff --git a/backend/utils/file_handling.go b/backend/utils/file_handling.go
new file mode 100644 (file)
index 0000000..f8989dd
--- /dev/null
@@ -0,0 +1,623 @@
+package utils
+
+import (
+       "crypto/cipher"
+       "crypto/rand"
+       "encoding/base64"
+       "encoding/json"
+       "fmt"
+       "io"
+       "os"
+       "path/filepath"
+       "strconv"
+       "strings"
+
+       "golang.org/x/crypto/chacha20poly1305"
+)
+
+// GetUsers retrieves the users from the users.json file
+func GetUsers() (map[string]any, error) {
+       // Try to open the users.json file
+       filePath := filepath.Join(Settings.DataPath, "users.json")
+       file, err := os.Open(filePath)
+       if err != nil {
+               if os.IsNotExist(err) {
+                       Logger.Printf("users.json - File not found")
+                       return map[string]any{}, nil
+               }
+               Logger.Printf("Error opening users.json: %v", err)
+               return nil, fmt.Errorf("internal server error when trying to open users.json")
+       }
+       defer file.Close()
+
+       // Read the file content
+       var content map[string]any
+       decoder := json.NewDecoder(file)
+       if err := decoder.Decode(&content); err != nil {
+               if err == io.EOF {
+                       return map[string]any{}, nil
+               }
+               Logger.Printf("Error decoding users.json: %v", err)
+               return nil, fmt.Errorf("internal server error when trying to decode users.json")
+       }
+
+       return content, nil
+}
+
+// WriteUsers writes the users to the users.json file
+func WriteUsers(content map[string]any) error {
+       // Create the users.json file
+       filePath := filepath.Join(Settings.DataPath, "users.json")
+       file, err := os.Create(filePath)
+       if err != nil {
+               Logger.Printf("Error creating users.json: %v", err)
+               return fmt.Errorf("internal server error when trying to create users.json")
+       }
+       defer file.Close()
+
+       // Write the content to the file
+       var encoder *json.Encoder
+       if Settings.Development && Settings.Indent > 0 {
+               encoder = json.NewEncoder(file)
+               encoder.SetIndent("", fmt.Sprintf("%*s", Settings.Indent, ""))
+       } else {
+               encoder = json.NewEncoder(file)
+       }
+
+       if err := encoder.Encode(content); err != nil {
+               Logger.Printf("Error encoding users.json: %v", err)
+               return fmt.Errorf("internal server error when trying to encode users.json")
+       }
+
+       return nil
+}
+
+// GetMonth retrieves the logs for a specific month
+func GetMonth(userID int, year, month int) (map[string]any, error) {
+       // Try to open the month.json file
+       filePath := filepath.Join(Settings.DataPath, fmt.Sprintf("%d/%d/%02d.json", userID, year, month))
+       file, err := os.Open(filePath)
+       if err != nil {
+               if os.IsNotExist(err) {
+                       Logger.Printf("%s - File not found", filePath)
+                       return map[string]any{}, nil
+               }
+               Logger.Printf("Error opening %s: %v", filePath, err)
+               return nil, fmt.Errorf("internal server error when trying to open %d/%02d.json", year, month)
+       }
+       defer file.Close()
+
+       // Read the file content
+       var content map[string]any
+       decoder := json.NewDecoder(file)
+       if err := decoder.Decode(&content); err != nil {
+               if err == io.EOF {
+                       return map[string]any{}, nil
+               }
+               Logger.Printf("Error decoding %s: %v", filePath, err)
+               return nil, fmt.Errorf("internal server error when trying to decode %d/%02d.json", year, month)
+       }
+
+       return content, nil
+}
+
+// WriteMonth writes the logs for a specific month
+func WriteMonth(userID int, year, month int, content map[string]any) error {
+       // Create the directory if it doesn't exist
+       dirPath := filepath.Join(Settings.DataPath, fmt.Sprintf("%d/%d", userID, year))
+       if err := os.MkdirAll(dirPath, 0755); err != nil {
+               Logger.Printf("Error creating directory %s: %v", dirPath, err)
+               return fmt.Errorf("internal server error when trying to create directory %d/%d", userID, year)
+       }
+
+       // Create the month.json file
+       filePath := filepath.Join(dirPath, fmt.Sprintf("%02d.json", month))
+       file, err := os.Create(filePath)
+       if err != nil {
+               Logger.Printf("Error creating %s: %v", filePath, err)
+               return fmt.Errorf("internal server error when trying to create %d/%02d.json", year, month)
+       }
+       defer file.Close()
+
+       // Write the content to the file
+       var encoder *json.Encoder
+       if Settings.Development && Settings.Indent > 0 {
+               encoder = json.NewEncoder(file)
+               encoder.SetIndent("", fmt.Sprintf("%*s", Settings.Indent, ""))
+       } else {
+               encoder = json.NewEncoder(file)
+       }
+
+       if err := encoder.Encode(content); err != nil {
+               Logger.Printf("Error encoding %s: %v", filePath, err)
+               return fmt.Errorf("internal server error when trying to encode %d/%02d.json", year, month)
+       }
+
+       return nil
+}
+
+// GetTags retrieves the tags for a specific user
+func GetTags(userID int) (map[string]any, error) {
+       // Try to open the tags.json file
+       filePath := filepath.Join(Settings.DataPath, fmt.Sprintf("%d/tags.json", userID))
+       file, err := os.Open(filePath)
+       if err != nil {
+               if os.IsNotExist(err) {
+                       Logger.Printf("%s - File not found", filePath)
+                       return map[string]any{}, nil
+               }
+               Logger.Printf("Error opening %s: %v", filePath, err)
+               return nil, fmt.Errorf("internal server error when trying to open tags.json")
+       }
+       defer file.Close()
+
+       // Read the file content
+       var content map[string]any
+       decoder := json.NewDecoder(file)
+       if err := decoder.Decode(&content); err != nil {
+               if err == io.EOF {
+                       return map[string]any{}, nil
+               }
+               Logger.Printf("Error decoding %s: %v", filePath, err)
+               return nil, fmt.Errorf("internal server error when trying to decode tags.json")
+       }
+
+       return content, nil
+}
+
+// WriteTags writes the tags for a specific user
+func WriteTags(userID int, content map[string]any) error {
+       // Create the directory if it doesn't exist
+       dirPath := filepath.Join(Settings.DataPath, fmt.Sprintf("%d", userID))
+       if err := os.MkdirAll(dirPath, 0755); err != nil {
+               Logger.Printf("Error creating directory %s: %v", dirPath, err)
+               return fmt.Errorf("internal server error when trying to create directory %d", userID)
+       }
+
+       // Create the tags.json file
+       filePath := filepath.Join(dirPath, "tags.json")
+       file, err := os.Create(filePath)
+       if err != nil {
+               Logger.Printf("Error creating %s: %v", filePath, err)
+               return fmt.Errorf("internal server error when trying to create tags.json")
+       }
+       defer file.Close()
+
+       // Write the content to the file
+       var encoder *json.Encoder
+       if Settings.Development && Settings.Indent > 0 {
+               encoder = json.NewEncoder(file)
+               encoder.SetIndent("", fmt.Sprintf("%*s", Settings.Indent, ""))
+       } else {
+               encoder = json.NewEncoder(file)
+       }
+
+       if err := encoder.Encode(content); err != nil {
+               Logger.Printf("Error encoding %s: %v", filePath, err)
+               return fmt.Errorf("internal server error when trying to encode tags.json")
+       }
+
+       return nil
+}
+
+// RandRead is a helper function for reading random bytes
+func RandRead(b []byte) (int, error) {
+       return rand.Read(b)
+}
+
+// GetUserSettings retrieves the settings for a specific user
+func GetUserSettings(userID int) (string, error) {
+       // Try to open the settings.encrypted file
+       filePath := filepath.Join(Settings.DataPath, fmt.Sprintf("%d/settings.encrypted", userID))
+       file, err := os.Open(filePath)
+       if err != nil {
+               if os.IsNotExist(err) {
+                       Logger.Printf("%s - File not found", filePath)
+                       return "", nil
+               }
+               Logger.Printf("Error opening %s: %v", filePath, err)
+               return "", fmt.Errorf("internal server error when trying to open settings.encrypted")
+       }
+       defer file.Close()
+
+       // Read the file content
+       content, err := io.ReadAll(file)
+       if err != nil {
+               Logger.Printf("Error reading %s: %v", filePath, err)
+               return "", fmt.Errorf("internal server error when trying to read settings.encrypted")
+       }
+
+       return string(content), nil
+}
+
+// WriteUserSettings writes the settings for a specific user
+func WriteUserSettings(userID int, content string) error {
+       // Create the directory if it doesn't exist
+       dirPath := filepath.Join(Settings.DataPath, fmt.Sprintf("%d", userID))
+       if err := os.MkdirAll(dirPath, 0755); err != nil {
+               Logger.Printf("Error creating directory %s: %v", dirPath, err)
+               return fmt.Errorf("internal server error when trying to create directory %d", userID)
+       }
+
+       // Create the settings.encrypted file
+       filePath := filepath.Join(dirPath, "settings.encrypted")
+       file, err := os.Create(filePath)
+       if err != nil {
+               Logger.Printf("Error creating %s: %v", filePath, err)
+               return fmt.Errorf("internal server error when trying to create settings.encrypted")
+       }
+       defer file.Close()
+
+       // Write the content to the file
+       if _, err := file.WriteString(content); err != nil {
+               Logger.Printf("Error writing %s: %v", filePath, err)
+               return fmt.Errorf("internal server error when trying to write settings.encrypted")
+       }
+
+       return nil
+}
+
+// GetTemplates retrieves the templates for a specific user
+func GetTemplates(userID int) (map[string]any, error) {
+       // Try to open the templates.json file
+       filePath := filepath.Join(Settings.DataPath, fmt.Sprintf("%d/templates.json", userID))
+       file, err := os.Open(filePath)
+       if err != nil {
+               if os.IsNotExist(err) {
+                       Logger.Printf("%s - File not found", filePath)
+                       return map[string]any{}, nil
+               }
+               Logger.Printf("Error opening %s: %v", filePath, err)
+               return nil, fmt.Errorf("internal server error when trying to open templates.json")
+       }
+       defer file.Close()
+
+       // Read the file content
+       var content map[string]any
+       decoder := json.NewDecoder(file)
+       if err := decoder.Decode(&content); err != nil {
+               if err == io.EOF {
+                       return map[string]any{}, nil
+               }
+               Logger.Printf("Error decoding %s: %v", filePath, err)
+               return nil, fmt.Errorf("internal server error when trying to decode templates.json")
+       }
+
+       return content, nil
+}
+
+// WriteTemplates writes the templates for a specific user
+func WriteTemplates(userID int, content map[string]any) error {
+       // Create the directory if it doesn't exist
+       dirPath := filepath.Join(Settings.DataPath, fmt.Sprintf("%d", userID))
+       if err := os.MkdirAll(dirPath, 0755); err != nil {
+               Logger.Printf("Error creating directory %s: %v", dirPath, err)
+               return fmt.Errorf("internal server error when trying to create directory %d", userID)
+       }
+
+       // Create the templates.json file
+       filePath := filepath.Join(dirPath, "templates.json")
+       file, err := os.Create(filePath)
+       if err != nil {
+               Logger.Printf("Error creating %s: %v", filePath, err)
+               return fmt.Errorf("internal server error when trying to create templates.json")
+       }
+       defer file.Close()
+
+       // Write the content to the file
+       var encoder *json.Encoder
+       if Settings.Development && Settings.Indent > 0 {
+               encoder = json.NewEncoder(file)
+               encoder.SetIndent("", fmt.Sprintf("%*s", Settings.Indent, ""))
+       } else {
+               encoder = json.NewEncoder(file)
+       }
+
+       if err := encoder.Encode(content); err != nil {
+               Logger.Printf("Error encoding %s: %v", filePath, err)
+               return fmt.Errorf("internal server error when trying to encode templates.json")
+       }
+
+       return nil
+}
+
+// WriteFile writes a file for a specific user
+func WriteFile(content []byte, userID int, uuid string) error {
+       // Create the directory if it doesn't exist
+       dirPath := filepath.Join(Settings.DataPath, fmt.Sprintf("%d/files", userID))
+       if err := os.MkdirAll(dirPath, 0755); err != nil {
+               Logger.Printf("Error creating directory %s: %v", dirPath, err)
+               return fmt.Errorf("internal server error when trying to create directory %d/files", userID)
+       }
+
+       // Create the file
+       filePath := filepath.Join(dirPath, uuid)
+       file, err := os.Create(filePath)
+       if err != nil {
+               Logger.Printf("Error creating %s: %v", filePath, err)
+               return fmt.Errorf("internal server error when trying to create file %s", uuid)
+       }
+       defer file.Close()
+
+       // Write the content to the file
+       if _, err := file.Write(content); err != nil {
+               Logger.Printf("Error writing %s: %v", filePath, err)
+               return fmt.Errorf("internal server error when trying to write file %s", uuid)
+       }
+
+       return nil
+}
+
+// ReadFile reads a file for a specific user
+func ReadFile(userID int, uuid string) ([]byte, error) {
+       // Try to open the file
+       filePath := filepath.Join(Settings.DataPath, fmt.Sprintf("%d/files/%s", userID, uuid))
+       file, err := os.Open(filePath)
+       if err != nil {
+               if os.IsNotExist(err) {
+                       Logger.Printf("%s - File not found", filePath)
+                       return nil, fmt.Errorf("file not found")
+               }
+               Logger.Printf("Error opening %s: %v", filePath, err)
+               return nil, fmt.Errorf("internal server error when trying to open file %s", uuid)
+       }
+       defer file.Close()
+
+       // Read the file content
+       content, err := io.ReadAll(file)
+       if err != nil {
+               Logger.Printf("Error reading %s: %v", filePath, err)
+               return nil, fmt.Errorf("internal server error when trying to read file %s", uuid)
+       }
+
+       return content, nil
+}
+
+// RemoveFile removes a file for a specific user
+func RemoveFile(userID int, uuid string) error {
+       // Try to remove the file
+       filePath := filepath.Join(Settings.DataPath, fmt.Sprintf("%d/files/%s", userID, uuid))
+       if err := os.Remove(filePath); err != nil {
+               Logger.Printf("Error removing %s: %v", filePath, err)
+               return fmt.Errorf("internal server error when trying to remove file %s", uuid)
+       }
+
+       return nil
+}
+
+// GetYears returns the years available for a specific user
+func GetYears(userID int) ([]string, error) {
+       // Try to read the user directory
+       dirPath := filepath.Join(Settings.DataPath, fmt.Sprintf("%d", userID))
+       entries, err := os.ReadDir(dirPath)
+       if err != nil {
+               if os.IsNotExist(err) {
+                       Logger.Printf("%s - Directory not found", dirPath)
+                       return []string{}, nil
+               }
+               Logger.Printf("Error reading directory %s: %v", dirPath, err)
+               return nil, fmt.Errorf("internal server error when trying to read directory %d", userID)
+       }
+
+       // Filter years
+       years := []string{}
+       for _, entry := range entries {
+               if entry.IsDir() && len(entry.Name()) == 4 {
+                       // Check if the name is a valid year (4 digits)
+                       if _, err := strconv.Atoi(entry.Name()); err == nil {
+                               years = append(years, entry.Name())
+                       }
+               }
+       }
+
+       return years, nil
+}
+
+// GetMonths returns the months available for a specific user and year
+func GetMonths(userID int, year string) ([]string, error) {
+       // Try to read the year directory
+       dirPath := filepath.Join(Settings.DataPath, fmt.Sprintf("%d/%s", userID, year))
+       entries, err := os.ReadDir(dirPath)
+       if err != nil {
+               if os.IsNotExist(err) {
+                       Logger.Printf("%s - Directory not found", dirPath)
+                       return []string{}, nil
+               }
+               Logger.Printf("Error reading directory %s: %v", dirPath, err)
+               return nil, fmt.Errorf("internal server error when trying to read directory %d/%s", userID, year)
+       }
+
+       // Filter months
+       months := []string{}
+       for _, entry := range entries {
+               if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".json") {
+                       // Extract month from filename (remove .json)
+                       month := strings.TrimSuffix(entry.Name(), ".json")
+                       months = append(months, month)
+               }
+       }
+
+       return months, nil
+}
+
+// CreateAEAD creates an AEAD cipher for encryption/decryption
+func CreateAEAD(key []byte) (cipher.AEAD, error) {
+       return chacha20poly1305.New(key)
+}
+
+// EncryptText encrypts text using the provided key
+func EncryptText(text, key string) (string, error) {
+       // Decode key
+       keyBytes, err := base64.URLEncoding.DecodeString(key)
+       if err != nil {
+               return "", fmt.Errorf("error decoding key: %v", err)
+       }
+
+       // Create AEAD cipher
+       aead, err := chacha20poly1305.New(keyBytes)
+       if err != nil {
+               return "", fmt.Errorf("error creating cipher: %v", err)
+       }
+
+       // Create nonce
+       nonce := make([]byte, aead.NonceSize())
+       if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
+               return "", fmt.Errorf("error creating nonce: %v", err)
+       }
+
+       // Encrypt text
+       ciphertext := aead.Seal(nonce, nonce, []byte(text), nil)
+       return base64.URLEncoding.EncodeToString(ciphertext), nil
+}
+
+// DecryptText decrypts text using the provided key
+func DecryptText(ciphertext, key string) (string, error) {
+       // Decode key and ciphertext
+       keyBytes, err := base64.URLEncoding.DecodeString(key)
+       if err != nil {
+               return "", fmt.Errorf("error decoding key: %v", err)
+       }
+
+       ciphertextBytes, err := base64.URLEncoding.DecodeString(ciphertext)
+       if err != nil {
+               return "", fmt.Errorf("error decoding ciphertext: %v", err)
+       }
+
+       // Create AEAD cipher
+       aead, err := chacha20poly1305.New(keyBytes)
+       if err != nil {
+               return "", fmt.Errorf("error creating cipher: %v", err)
+       }
+
+       // Extract nonce from ciphertext
+       if len(ciphertextBytes) < aead.NonceSize() {
+               return "", fmt.Errorf("ciphertext too short")
+       }
+       nonce, ciphertextBytes := ciphertextBytes[:aead.NonceSize()], ciphertextBytes[aead.NonceSize():]
+
+       // Decrypt text
+       plaintext, err := aead.Open(nil, nonce, ciphertextBytes, nil)
+       if err != nil {
+               return "", fmt.Errorf("error decrypting ciphertext: %v", err)
+       }
+
+       return string(plaintext), nil
+}
+
+// EncryptFile encrypts a file using the provided key
+func EncryptFile(data []byte, key string) ([]byte, error) {
+       // Decode key
+       keyBytes, err := base64.URLEncoding.DecodeString(key)
+       if err != nil {
+               return nil, fmt.Errorf("error decoding key: %v", err)
+       }
+
+       // Create AEAD cipher
+       aead, err := chacha20poly1305.New(keyBytes)
+       if err != nil {
+               return nil, fmt.Errorf("error creating cipher: %v", err)
+       }
+
+       // Create nonce
+       nonce := make([]byte, aead.NonceSize())
+       if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
+               return nil, fmt.Errorf("error creating nonce: %v", err)
+       }
+
+       // Encrypt file
+       ciphertext := aead.Seal(nonce, nonce, data, nil)
+       return ciphertext, nil
+}
+
+// DecryptFile decrypts a file using the provided key
+func DecryptFile(ciphertext []byte, key string) ([]byte, error) {
+       // Decode key
+       keyBytes, err := base64.URLEncoding.DecodeString(key)
+       if err != nil {
+               return nil, fmt.Errorf("error decoding key: %v", err)
+       }
+
+       // Create AEAD cipher
+       aead, err := chacha20poly1305.New(keyBytes)
+       if err != nil {
+               return nil, fmt.Errorf("error creating cipher: %v", err)
+       }
+
+       // Extract nonce from ciphertext
+       if len(ciphertext) < aead.NonceSize() {
+               return nil, fmt.Errorf("ciphertext too short")
+       }
+       nonce, ciphertext := ciphertext[:aead.NonceSize()], ciphertext[aead.NonceSize():]
+
+       // Decrypt file
+       plaintext, err := aead.Open(nil, nonce, ciphertext, nil)
+       if err != nil {
+               return nil, fmt.Errorf("error decrypting ciphertext: %v", err)
+       }
+
+       return plaintext, nil
+}
+
+// GetEncryptionKey retrieves the encryption key for a specific user
+func GetEncryptionKey(userID int, derivedKey string) (string, error) {
+       // Get users
+       users, err := GetUsers()
+       if err != nil {
+               return "", fmt.Errorf("error retrieving users: %v", err)
+       }
+
+       // Find user
+       usersList, ok := users["users"].([]any)
+       if !ok {
+               return "", fmt.Errorf("users.json is not in the correct format")
+       }
+
+       for _, u := range usersList {
+               user, ok := u.(map[string]any)
+               if !ok {
+                       continue
+               }
+
+               if id, ok := user["user_id"].(float64); ok && int(id) == userID {
+                       encEncKey, ok := user["enc_enc_key"].(string)
+                       if !ok {
+                               return "", fmt.Errorf("user data is not in the correct format")
+                       }
+
+                       // Decode derived key
+                       derivedKeyBytes, err := base64.StdEncoding.DecodeString(derivedKey)
+                       if err != nil {
+                               return "", fmt.Errorf("error decoding derived key: %v", err)
+                       }
+
+                       // Create Fernet cipher
+                       aead, err := CreateAEAD(derivedKeyBytes)
+                       if err != nil {
+                               return "", fmt.Errorf("error creating cipher: %v", err)
+                       }
+
+                       // Decode encrypted key
+                       encEncKeyBytes, err := base64.StdEncoding.DecodeString(encEncKey)
+                       if err != nil {
+                               return "", fmt.Errorf("error decoding encrypted key: %v", err)
+                       }
+
+                       // Extract nonce from encrypted key
+                       if len(encEncKeyBytes) < aead.NonceSize() {
+                               return "", fmt.Errorf("encrypted key too short")
+                       }
+                       nonce, encKeyBytes := encEncKeyBytes[:aead.NonceSize()], encEncKeyBytes[aead.NonceSize():]
+
+                       // Decrypt key
+                       keyBytes, err := aead.Open(nil, nonce, encKeyBytes, nil)
+                       if err != nil {
+                               return "", fmt.Errorf("error decrypting key: %v", err)
+                       }
+
+                       // Return base64-encoded key
+                       return base64.URLEncoding.EncodeToString(keyBytes), nil
+               }
+       }
+
+       return "", fmt.Errorf("user not found")
+}
diff --git a/backend/utils/security.go b/backend/utils/security.go
new file mode 100644 (file)
index 0000000..07e9e85
--- /dev/null
@@ -0,0 +1,243 @@
+package utils
+
+import (
+       "crypto/rand"
+       "encoding/base64"
+       "encoding/json"
+       "fmt"
+       "io"
+       "log"
+       "net/http"
+       "os"
+       "time"
+
+       "github.com/golang-jwt/jwt/v5"
+       "golang.org/x/crypto/argon2"
+)
+
+// Global logger
+var Logger *log.Logger
+
+func init() {
+       // Initialize logger
+       Logger = log.New(os.Stdout, "dailytxt: ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
+}
+
+// ContextKey is a type for context keys
+type ContextKey string
+
+// Context keys
+const (
+       UserIDKey     ContextKey = "userID"
+       UsernameKey   ContextKey = "username"
+       DerivedKeyKey ContextKey = "derivedKey"
+)
+
+// Settings holds the application settings
+type AppSettings struct {
+       DataPath        string   `json:"data_path"`
+       Development     bool     `json:"development"`
+       SecretToken     string   `json:"secret_token"`
+       LogoutAfterDays int      `json:"logout_after_days"`
+       AllowedHosts    []string `json:"allowed_hosts"`
+       Indent          int      `json:"indent"`
+}
+
+// Global settings
+var Settings AppSettings
+
+// InitSettings loads the application settings
+func InitSettings() error {
+       // Default settings
+       Settings = AppSettings{
+               DataPath:        "/data",
+               Development:     false,
+               SecretToken:     generateSecretToken(),
+               LogoutAfterDays: 30,
+               AllowedHosts:    []string{"http://localhost:5173", "http://127.0.0.1:5173"},
+               Indent:          0,
+       }
+
+       fmt.Print("\nDetected following settings:\n================\n")
+
+       // Override with environment variables if available
+       if dataPath := os.Getenv("DATA_PATH"); dataPath != "" {
+               Settings.DataPath = dataPath
+       }
+       fmt.Printf("Data Path: %s\n", Settings.DataPath)
+
+       if os.Getenv("DEVELOPMENT") == "true" {
+               Settings.Development = true
+       }
+       fmt.Printf("Development Mode: %t\n", Settings.Development)
+
+       if secretToken := os.Getenv("SECRET_TOKEN"); secretToken != "" {
+               Settings.SecretToken = secretToken
+       }
+       fmt.Printf("Secret Token: %s\n", Settings.SecretToken)
+
+       if logoutDays := os.Getenv("LOGOUT_AFTER_DAYS"); logoutDays != "" {
+               // Parse logoutDays to int
+               var days int
+               if _, err := fmt.Sscanf(logoutDays, "%d", &days); err == nil {
+                       Settings.LogoutAfterDays = days
+               }
+       }
+       fmt.Printf("Logout After Days: %d\n", Settings.LogoutAfterDays)
+
+       if indent := os.Getenv("INDENT"); indent != "" {
+               // Parse indent to int
+               var ind int
+               if _, err := fmt.Sscanf(indent, "%d", &ind); err == nil {
+                       Settings.Indent = ind
+               }
+       }
+       fmt.Printf("Indent: %d\n================\n\n", Settings.Indent)
+
+       // Create data directory if it doesn't exist
+       if err := os.MkdirAll(Settings.DataPath, 0755); err != nil {
+               return fmt.Errorf("failed to create data directory: %v", err)
+       }
+
+       return nil
+}
+
+// Claims represents the JWT claims
+type Claims struct {
+       UserID     int    `json:"user_id"`
+       Username   string `json:"name"`
+       DerivedKey string `json:"derived_key"`
+       jwt.RegisteredClaims
+}
+
+// GenerateToken creates a new JWT token
+func GenerateToken(userID int, username, derivedKey string) (string, error) {
+       // Create expiration time
+       expirationTime := time.Now().Add(time.Duration(Settings.LogoutAfterDays) * 24 * time.Hour)
+
+       // Create claims
+       claims := &Claims{
+               UserID:     userID,
+               Username:   username,
+               DerivedKey: derivedKey,
+               RegisteredClaims: jwt.RegisteredClaims{
+                       ExpiresAt: jwt.NewNumericDate(expirationTime),
+               },
+       }
+
+       // Create token
+       token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+
+       // Sign token with secret key
+       tokenString, err := token.SignedString([]byte(Settings.SecretToken))
+       if err != nil {
+               return "", err
+       }
+
+       return tokenString, nil
+}
+
+// ValidateToken validates a JWT token and returns the claims
+func ValidateToken(tokenString string) (*Claims, error) {
+       // Parse token
+       claims := &Claims{}
+       token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (any, error) {
+               // Validate signing method
+               if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
+                       return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
+               }
+
+               return []byte(Settings.SecretToken), nil
+       })
+
+       if err != nil {
+               return nil, err
+       }
+
+       if !token.Valid {
+               return nil, fmt.Errorf("invalid token")
+       }
+
+       return claims, nil
+}
+
+// HashPassword hashes a password using Argon2
+func HashPassword(password string) (string, string, error) {
+       // Generate a random salt
+       salt := make([]byte, 16)
+       if _, err := io.ReadFull(rand.Reader, salt); err != nil {
+               return "", "", err
+       }
+
+       // Hash password
+       hash := argon2.IDKey([]byte(password), salt, 2, 64*1024, 4, 32)
+
+       // Encode salt and hash to base64
+       saltBase64 := base64.StdEncoding.EncodeToString(salt)
+       hashBase64 := base64.StdEncoding.EncodeToString(hash)
+
+       return hashBase64, saltBase64, nil
+}
+
+// VerifyPassword verifies if a password matches a hash
+func VerifyPassword(password, hashBase64, saltBase64 string) bool {
+       // Decode salt and hash
+       salt, err := base64.StdEncoding.DecodeString(saltBase64)
+       if err != nil {
+               return false
+       }
+
+       _, err = base64.StdEncoding.DecodeString(hashBase64)
+       if err != nil {
+               return false
+       }
+
+       // Hash the provided password with the same salt
+       hash := argon2.IDKey([]byte(password), salt, 2, 64*1024, 4, 32)
+
+       // Compare hashes
+       return base64.StdEncoding.EncodeToString(hash) == hashBase64
+}
+
+// DeriveKeyFromPassword derives a key from a password and salt
+func DeriveKeyFromPassword(password, saltBase64 string) ([]byte, error) {
+       // Decode salt
+       salt, err := base64.StdEncoding.DecodeString(saltBase64)
+       if err != nil {
+               return nil, err
+       }
+
+       // Derive key
+       key := argon2.IDKey([]byte(password), salt, 2, 64*1024, 4, 32)
+       return key, nil
+}
+
+// GenerateSecretToken generates a secure random token
+func generateSecretToken() string {
+       b := make([]byte, 32)
+       if _, err := rand.Read(b); err != nil {
+               Logger.Fatalf("Failed to generate secret token: %v", err)
+       }
+       return base64.URLEncoding.EncodeToString(b)
+}
+
+// JSONResponse sends a JSON response with the given status code and data
+func JSONResponse(w http.ResponseWriter, statusCode int, data any) {
+       // Set content type
+       w.Header().Set("Content-Type", "application/json")
+       w.WriteHeader(statusCode)
+
+       // Encode data to JSON
+       var encoder *json.Encoder
+       if Settings.Development && Settings.Indent > 0 {
+               encoder = json.NewEncoder(w)
+               encoder.SetIndent("", fmt.Sprintf("%*s", Settings.Indent, ""))
+       } else {
+               encoder = json.NewEncoder(w)
+       }
+
+       if err := encoder.Encode(data); err != nil {
+               Logger.Printf("Error encoding JSON response: %v", err)
+               http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+       }
+}
git clone https://git.99rst.org/PROJECT