Files
sysops 3b05e949dd feat(PROJ-13,PROJ-42): REST API v1 + Gespeicherte Suchanfragen
PROJ-13: Externe REST API für CRM/ERP-Anbindung
- API-Key Middleware mit SHA-256-Hash-Lookup + Token-Bucket Rate-Limiter
- GET /api/v1/mails — Suche mit Paginierung (max 100/Seite)
- GET /api/v1/mails/{id} — Mail-Metadaten als JSON
- GET /api/v1/mails/{id}/raw — Original-EML Download
- Admin-Endpoints: POST/GET/DELETE /api/admin/apikeys
- Tenant-Isolation, Audit-Log, 405 für non-GET Methoden

PROJ-42: Gespeicherte Suchanfragen
- Tabelle saved_searches (user_id, tenant_id, name, query_json)
- GET/POST/DELETE /api/searches/saved mit Ownership-Check
- Frontend: "Suche speichern"-Button + Popover mit gespeicherten Suchen
- shadcn/ui Komponenten, Loading/Empty States

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 10:54:26 +02:00

77 lines
2.3 KiB
Go

package storage
import (
"context"
"fmt"
"time"
)
// SavedSearch represents a user's saved search query (PROJ-42).
type SavedSearch struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
TenantID int64 `json:"tenant_id"`
Name string `json:"name"`
QueryJSON []byte `json:"query"`
CreatedAt time.Time `json:"created_at"`
}
// ListSavedSearches returns all saved searches for the given user and tenant.
func (s *Store) ListSavedSearches(ctx context.Context, userID, tenantID int64) ([]SavedSearch, error) {
rows, err := s.db.Query(ctx, `
SELECT id, user_id, tenant_id, name, query_json, created_at
FROM saved_searches
WHERE user_id = $1 AND tenant_id = $2
ORDER BY created_at DESC
`, userID, tenantID)
if err != nil {
return nil, fmt.Errorf("saved_searches: list: %w", err)
}
defer rows.Close()
var result []SavedSearch
for rows.Next() {
var ss SavedSearch
if err := rows.Scan(&ss.ID, &ss.UserID, &ss.TenantID, &ss.Name, &ss.QueryJSON, &ss.CreatedAt); err != nil {
return nil, fmt.Errorf("saved_searches: scan: %w", err)
}
result = append(result, ss)
}
return result, rows.Err()
}
// CreateSavedSearch inserts a new saved search and returns it.
func (s *Store) CreateSavedSearch(ctx context.Context, userID, tenantID int64, name string, queryJSON []byte) (*SavedSearch, error) {
ss := &SavedSearch{
UserID: userID,
TenantID: tenantID,
Name: name,
QueryJSON: queryJSON,
}
err := s.db.QueryRow(ctx, `
INSERT INTO saved_searches (user_id, tenant_id, name, query_json)
VALUES ($1, $2, $3, $4)
RETURNING id, created_at
`, userID, tenantID, name, queryJSON).Scan(&ss.ID, &ss.CreatedAt)
if err != nil {
return nil, fmt.Errorf("saved_searches: create: %w", err)
}
return ss, nil
}
// DeleteSavedSearch deletes a saved search. Ownership is enforced by requiring
// both userID and tenantID to match the row.
func (s *Store) DeleteSavedSearch(ctx context.Context, id, userID, tenantID int64) error {
tag, err := s.db.Exec(ctx, `
DELETE FROM saved_searches
WHERE id = $1 AND user_id = $2 AND tenant_id = $3
`, id, userID, tenantID)
if err != nil {
return fmt.Errorf("saved_searches: delete: %w", err)
}
if tag.RowsAffected() == 0 {
return fmt.Errorf("saved_searches: not found or not owned by user")
}
return nil
}