diff --git a/features/INDEX.md b/features/INDEX.md index 205dcee..df1daca 100644 --- a/features/INDEX.md +++ b/features/INDEX.md @@ -70,7 +70,8 @@ | PROJ-51 | Aufbewahrungsfristen nach Dokumentenart (Retention-Kategorien) | Deployed | [PROJ-51](PROJ-51-retention-kategorien.md) | 2026-06-13 | | PROJ-52 | Vollständigkeits-Reconciliation (Zähl-Report) | Planned | [PROJ-52](PROJ-52-vollstaendigkeits-reconciliation.md) | 2026-06-13 | | PROJ-53 | Konfigurierbare Listenanzahl pro Seite | In Review | [PROJ-53](PROJ-53-konfigurierbare-listenanzahl.md) | 2026-06-14 | +| PROJ-54 | Fix Listenansicht/Pagination für Rolle "user" (Nachbesserung PROJ-6/PROJ-21) | In Review | [PROJ-54](PROJ-54-fix-listenansicht-total.md) | 2026-06-14 | -## Next Available ID: PROJ-54 +## Next Available ID: PROJ-55 diff --git a/internal/api/search_handlers.go b/internal/api/search_handlers.go index 8b69d21..4ecb19b 100644 --- a/internal/api/search_handlers.go +++ b/internal/api/search_handlers.go @@ -87,6 +87,20 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) { return } + // PROJ-54: For user role, restrict results to mails the user is involved + // in (From/To/CC/BCC) at the INDEX level — before LIMIT/OFFSET — so that + // Total and pagination reflect the user's own mails, not the full + // tenant/global result set. Email comes from the JWT session — no DB + // lookup needed. If email is missing, block all results (fail-safe). + if sess.Role == userstore.RoleUser { + userEmailFilter := strings.ToLower(sess.Email) + if userEmailFilter == "" { + writeJSON(w, http.StatusOK, map[string]interface{}{"total": 0, "hits": []interface{}{}}) + return + } + req.AnyAddress = userEmailFilter + } + // PROJ-21 Phase 4: Use per-tenant index when available; fall back to // global index + post-filter when the tenant index manager is not wired. // auditor always uses the global index — they see no-tenant mails only, @@ -164,18 +178,6 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) { } } - // SEC: For user role, restrict results to mails the user is involved in - // (From, To, or CC). Email comes from the JWT session — no DB lookup needed. - // If email is missing, block all results (fail-safe). - var userEmailFilter string - if sess.Role == userstore.RoleUser { - userEmailFilter = strings.ToLower(sess.Email) - if userEmailFilter == "" { - writeJSON(w, http.StatusOK, map[string]interface{}{"total": 0, "hits": []interface{}{}}) - return - } - } - // Batch-load thread info and received_at fallback for all hits hitIDs := make([]string, len(result.Hits)) for i, h := range result.Hits { @@ -201,14 +203,6 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) { eh.Date = t.UTC().Format(time.RFC3339) } eh.HasAttachments = len(pm.Attachments) > 0 - - // User isolation: skip mails the user is not involved in. - if userEmailFilter != "" && !mailBelongsToUser(pm, userEmailFilter) { - continue - } - } else if userEmailFilter != "" { - // If mail can't be parsed, deny access to user role. - continue } // Auditor isolation: skip mails that belong to a tenant. if auditorAllowedIDs != nil {