From 3512783ea75ce42fd289f013ec00bb85e06b02a7 Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 19 Jan 2026 11:39:27 -0800 Subject: [PATCH 1/2] security: add authentication to logs endpoint and filter by user Add session authentication to /sync/logs endpoint and filter logs by user UUID so users can only see their own logs. - Require valid session to access logs endpoint - Add GetLogsByUser() to filter logs by user UUID - Return 401 Unauthorized for unauthenticated requests - Update SyncLogsHandler signature to accept session store Co-Authored-By: Claude Opus 4.5 --- backend/controllers/get_logs.go | 65 ++++++++++++++++++++++----------- backend/main.go | 2 +- backend/models/logs.go | 27 ++++++++++++++ 3 files changed, 71 insertions(+), 23 deletions(-) diff --git a/backend/controllers/get_logs.go b/backend/controllers/get_logs.go index 42a21886..1a7830b5 100644 --- a/backend/controllers/get_logs.go +++ b/backend/controllers/get_logs.go @@ -5,43 +5,64 @@ import ( "encoding/json" "net/http" "strconv" + + "github.com/gorilla/sessions" ) // SyncLogsHandler godoc // @Summary Get sync logs -// @Description Fetch the latest sync operation logs +// @Description Fetch the latest sync operation logs for the authenticated user // @Tags Logs // @Accept json // @Produce json // @Param last query int false "Number of latest log entries to return (default: 100)" // @Success 200 {array} models.LogEntry "List of log entries" // @Failure 400 {string} string "Invalid last parameter" +// @Failure 401 {string} string "Authentication required" // @Router /sync/logs [get] -func SyncLogsHandler(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) - return - } +func SyncLogsHandler(store *sessions.CookieStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Invalid request method", http.StatusMethodNotAllowed) + return + } - // Get the 'last' query parameter, default to 100 - lastParam := r.URL.Query().Get("last") - last := 100 - if lastParam != "" { - parsedLast, err := strconv.Atoi(lastParam) - if err != nil || parsedLast < 0 { - http.Error(w, "Invalid 'last' parameter", http.StatusBadRequest) + // Validate session - user must be authenticated to view logs + session, err := store.Get(r, "session-name") + if err != nil { + http.Error(w, "Authentication required", http.StatusUnauthorized) return } - last = parsedLast - } - // Get the log store and retrieve logs - logStore := models.GetLogStore() - logs := logStore.GetLogs(last) + userInfo, ok := session.Values["user"].(map[string]interface{}) + if !ok || userInfo == nil { + http.Error(w, "Authentication required", http.StatusUnauthorized) + return + } - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(logs); err != nil { - http.Error(w, "Failed to encode logs", http.StatusInternalServerError) - return + // Get user's UUID to filter logs + userUUID, _ := userInfo["uuid"].(string) + + // Get the 'last' query parameter, default to 100 + lastParam := r.URL.Query().Get("last") + last := 100 + if lastParam != "" { + parsedLast, err := strconv.Atoi(lastParam) + if err != nil || parsedLast < 0 { + http.Error(w, "Invalid 'last' parameter", http.StatusBadRequest) + return + } + last = parsedLast + } + + // Get the log store and retrieve logs filtered by user UUID + logStore := models.GetLogStore() + logs := logStore.GetLogsByUser(last, userUUID) + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(logs); err != nil { + http.Error(w, "Failed to encode logs", http.StatusInternalServerError) + return + } } } diff --git a/backend/main.go b/backend/main.go index 00057bd1..99508fa4 100644 --- a/backend/main.go +++ b/backend/main.go @@ -109,7 +109,7 @@ func main() { mux.Handle("/modify-task", rateLimitedHandler(http.HandlerFunc(controllers.ModifyTaskHandler))) mux.Handle("/complete-task", rateLimitedHandler(http.HandlerFunc(controllers.CompleteTaskHandler))) mux.Handle("/delete-task", rateLimitedHandler(http.HandlerFunc(controllers.DeleteTaskHandler))) - mux.Handle("/sync/logs", rateLimitedHandler(http.HandlerFunc(controllers.SyncLogsHandler))) + mux.Handle("/sync/logs", rateLimitedHandler(controllers.SyncLogsHandler(store))) mux.Handle("/complete-tasks", rateLimitedHandler(http.HandlerFunc(controllers.BulkCompleteTaskHandler))) mux.Handle("/delete-tasks", rateLimitedHandler(http.HandlerFunc(controllers.BulkDeleteTaskHandler))) diff --git a/backend/models/logs.go b/backend/models/logs.go index 4fb5f73a..8977b88b 100644 --- a/backend/models/logs.go +++ b/backend/models/logs.go @@ -95,3 +95,30 @@ func (ls *LogStore) GetLogs(last int) []LogEntry { } return result } + +// GetLogsByUser returns the last N log entries for a specific user (filtered by SyncID/UUID) +func (ls *LogStore) GetLogsByUser(last int, userUUID string) []LogEntry { + ls.mu.RLock() + defer ls.mu.RUnlock() + + // Filter entries by user UUID + var userEntries []LogEntry + for _, entry := range ls.entries { + if entry.SyncID == userUUID { + userEntries = append(userEntries, entry) + } + } + + // Determine how many to return + count := last + if count <= 0 || count > len(userEntries) { + count = len(userEntries) + } + + // Return last N entries in reverse order (newest first) + result := make([]LogEntry, count) + for i := 0; i < count; i++ { + result[i] = userEntries[len(userEntries)-1-i] + } + return result +} From ccba488b21007618d9fe052f193b4159e946a161 Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 19 Jan 2026 12:55:51 -0800 Subject: [PATCH 2/2] fix: add hard cap of 20 to logs endpoint - Change default from 100 to 20 - Enforce maximum of 20 entries regardless of request - Prevents resource exhaustion from large requests Co-Authored-By: Claude Opus 4.5 --- backend/controllers/get_logs.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/backend/controllers/get_logs.go b/backend/controllers/get_logs.go index 1a7830b5..e7a4c010 100644 --- a/backend/controllers/get_logs.go +++ b/backend/controllers/get_logs.go @@ -15,7 +15,7 @@ import ( // @Tags Logs // @Accept json // @Produce json -// @Param last query int false "Number of latest log entries to return (default: 100)" +// @Param last query int false "Number of latest log entries to return (default: 20, max: 20)" // @Success 200 {array} models.LogEntry "List of log entries" // @Failure 400 {string} string "Invalid last parameter" // @Failure 401 {string} string "Authentication required" @@ -43,9 +43,10 @@ func SyncLogsHandler(store *sessions.CookieStore) http.HandlerFunc { // Get user's UUID to filter logs userUUID, _ := userInfo["uuid"].(string) - // Get the 'last' query parameter, default to 100 + // Get the 'last' query parameter, default to 20, max 20 + const maxLogs = 20 lastParam := r.URL.Query().Get("last") - last := 100 + last := maxLogs if lastParam != "" { parsedLast, err := strconv.Atoi(lastParam) if err != nil || parsedLast < 0 { @@ -54,6 +55,10 @@ func SyncLogsHandler(store *sessions.CookieStore) http.HandlerFunc { } last = parsedLast } + // Enforce hard cap to prevent resource exhaustion + if last > maxLogs { + last = maxLogs + } // Get the log store and retrieve logs filtered by user UUID logStore := models.GetLogStore()