--- /dev/null
+#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
--- /dev/null
+{
+ "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="
+ }
+ ]
+}
--- /dev/null
+{"id_counter":1,"users":[{"dailytxt_version":2,"enc_enc_key":"F72a95Ro/OP1Oa0dwNtbFUrpw9o7f1X3P73fNwj+ZzYqPGj8j94D48ewYc+CZ4Db3NrtBipVRKOXrKyq","password":"SgmHrbBGbhaQ4ibSq4GNcD3PES46kg4pB0xqBZdAjB0=","salt":"dj9yX4A0r6U6zkG7Ln4LhA==","user_id":1,"username":"username"}]}
--- /dev/null
+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
--- /dev/null
+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=
--- /dev/null
+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)
+}
--- /dev/null
+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)
+}
--- /dev/null
+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), ¤tSettings); err != nil {
+ http.Error(w, fmt.Sprintf("Error parsing settings: %v", err), http.StatusInternalServerError)
+ return
+ }
+ }
+
+ // If no settings or empty, use defaults
+ if len(currentSettings) == 0 {
+ currentSettings = 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,
+ })
+}
--- /dev/null
+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")
+}
--- /dev/null
+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)
+ })
+}
--- /dev/null
+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")
+}
--- /dev/null
+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)
+ }
+}