Compare commits
1 Commits
v2.6.93
...
v3.0.20260
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7009b24568 |
@@ -199,6 +199,7 @@ func run() error {
|
|||||||
BookWriter: store,
|
BookWriter: store,
|
||||||
AIJobStore: store,
|
AIJobStore: store,
|
||||||
BookAdminStore: store,
|
BookAdminStore: store,
|
||||||
|
NotificationStore: store,
|
||||||
Log: log,
|
Log: log,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package backend
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/libnovel/backend/internal/storage"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// handleDismissNotification handles DELETE /api/notifications/{id}.
|
// handleDismissNotification handles DELETE /api/notifications/{id}.
|
||||||
@@ -14,12 +12,11 @@ func (s *Server) handleDismissNotification(w http.ResponseWriter, r *http.Reques
|
|||||||
jsonError(w, http.StatusBadRequest, "notification id required")
|
jsonError(w, http.StatusBadRequest, "notification id required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
store, ok := s.deps.Producer.(*storage.Store)
|
if s.deps.NotificationStore == nil {
|
||||||
if !ok {
|
jsonError(w, http.StatusServiceUnavailable, "notification store not configured")
|
||||||
jsonError(w, http.StatusInternalServerError, "storage not available")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := store.DeleteNotification(r.Context(), id); err != nil {
|
if err := s.deps.NotificationStore.DeleteNotification(r.Context(), id); err != nil {
|
||||||
jsonError(w, http.StatusInternalServerError, "dismiss notification: "+err.Error())
|
jsonError(w, http.StatusInternalServerError, "dismiss notification: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -33,12 +30,11 @@ func (s *Server) handleClearAllNotifications(w http.ResponseWriter, r *http.Requ
|
|||||||
jsonError(w, http.StatusBadRequest, "user_id required")
|
jsonError(w, http.StatusBadRequest, "user_id required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
store, ok := s.deps.Producer.(*storage.Store)
|
if s.deps.NotificationStore == nil {
|
||||||
if !ok {
|
jsonError(w, http.StatusServiceUnavailable, "notification store not configured")
|
||||||
jsonError(w, http.StatusInternalServerError, "storage not available")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := store.ClearAllNotifications(r.Context(), userID); err != nil {
|
if err := s.deps.NotificationStore.ClearAllNotifications(r.Context(), userID); err != nil {
|
||||||
jsonError(w, http.StatusInternalServerError, "clear notifications: "+err.Error())
|
jsonError(w, http.StatusInternalServerError, "clear notifications: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -52,12 +48,11 @@ func (s *Server) handleMarkAllNotificationsRead(w http.ResponseWriter, r *http.R
|
|||||||
jsonError(w, http.StatusBadRequest, "user_id required")
|
jsonError(w, http.StatusBadRequest, "user_id required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
store, ok := s.deps.Producer.(*storage.Store)
|
if s.deps.NotificationStore == nil {
|
||||||
if !ok {
|
jsonError(w, http.StatusServiceUnavailable, "notification store not configured")
|
||||||
jsonError(w, http.StatusInternalServerError, "storage not available")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := store.MarkAllNotificationsRead(r.Context(), userID); err != nil {
|
if err := s.deps.NotificationStore.MarkAllNotificationsRead(r.Context(), userID); err != nil {
|
||||||
jsonError(w, http.StatusInternalServerError, "mark all read: "+err.Error())
|
jsonError(w, http.StatusInternalServerError, "mark all read: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -80,13 +75,12 @@ func (s *Server) handleListNotifications(w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
store, ok := s.deps.Producer.(*storage.Store)
|
if s.deps.NotificationStore == nil {
|
||||||
if !ok {
|
jsonError(w, http.StatusServiceUnavailable, "notification store not configured")
|
||||||
jsonError(w, http.StatusInternalServerError, "storage not available")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
items, err := store.ListNotifications(r.Context(), userID, 50)
|
items, err := s.deps.NotificationStore.ListNotifications(r.Context(), userID, 50)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonError(w, http.StatusInternalServerError, "list notifications: "+err.Error())
|
jsonError(w, http.StatusInternalServerError, "list notifications: "+err.Error())
|
||||||
return
|
return
|
||||||
@@ -97,7 +91,7 @@ func (s *Server) handleListNotifications(w http.ResponseWriter, r *http.Request)
|
|||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
b, _ := json.Marshal(item)
|
b, _ := json.Marshal(item)
|
||||||
var n notification
|
var n notification
|
||||||
json.Unmarshal(b, &n)
|
json.Unmarshal(b, &n) //nolint:errcheck
|
||||||
notifications = append(notifications, n)
|
notifications = append(notifications, n)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,16 +105,15 @@ func (s *Server) handleMarkNotificationRead(w http.ResponseWriter, r *http.Reque
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
store, ok := s.deps.Producer.(*storage.Store)
|
if s.deps.NotificationStore == nil {
|
||||||
if !ok {
|
jsonError(w, http.StatusServiceUnavailable, "notification store not configured")
|
||||||
jsonError(w, http.StatusInternalServerError, "storage not available")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := store.MarkNotificationRead(r.Context(), id); err != nil {
|
if err := s.deps.NotificationStore.MarkNotificationRead(r.Context(), id); err != nil {
|
||||||
jsonError(w, http.StatusInternalServerError, "mark read: "+err.Error())
|
jsonError(w, http.StatusInternalServerError, "mark read: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeJSON(w, 0, map[string]any{"success": true})
|
writeJSON(w, 0, map[string]any{"success": true})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,10 @@ type Dependencies struct {
|
|||||||
// BookAdminStore provides admin-only operations: archive, unarchive, hard-delete.
|
// BookAdminStore provides admin-only operations: archive, unarchive, hard-delete.
|
||||||
// If nil, the admin book management endpoints return 503.
|
// If nil, the admin book management endpoints return 503.
|
||||||
BookAdminStore bookstore.BookAdminStore
|
BookAdminStore bookstore.BookAdminStore
|
||||||
|
// NotificationStore manages per-user in-app notifications.
|
||||||
|
// Always wired directly to *storage.Store (not the Asynq wrapper) so
|
||||||
|
// notification endpoints work regardless of whether Redis/Asynq is in use.
|
||||||
|
NotificationStore bookstore.NotificationStore
|
||||||
// Log is the structured logger.
|
// Log is the structured logger.
|
||||||
Log *slog.Logger
|
Log *slog.Logger
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -247,3 +247,14 @@ type ImportFileStore interface {
|
|||||||
// GetImportChapters retrieves the pre-parsed chapters JSON.
|
// GetImportChapters retrieves the pre-parsed chapters JSON.
|
||||||
GetImportChapters(ctx context.Context, key string) ([]byte, error)
|
GetImportChapters(ctx context.Context, key string) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NotificationStore manages per-user in-app notifications.
|
||||||
|
// Always wired directly to the concrete *storage.Store so it works
|
||||||
|
// regardless of whether the Asynq task-queue wrapper is in use.
|
||||||
|
type NotificationStore interface {
|
||||||
|
ListNotifications(ctx context.Context, userID string, limit int) ([]map[string]any, error)
|
||||||
|
MarkNotificationRead(ctx context.Context, id string) error
|
||||||
|
MarkAllNotificationsRead(ctx context.Context, userID string) error
|
||||||
|
DeleteNotification(ctx context.Context, id string) error
|
||||||
|
ClearAllNotifications(ctx context.Context, userID string) error
|
||||||
|
}
|
||||||
|
|||||||
@@ -773,7 +773,7 @@ func (s *Store) CreateNotification(ctx context.Context, userID, title, message,
|
|||||||
|
|
||||||
// ListNotifications returns notifications for a user.
|
// ListNotifications returns notifications for a user.
|
||||||
func (s *Store) ListNotifications(ctx context.Context, userID string, limit int) ([]map[string]any, error) {
|
func (s *Store) ListNotifications(ctx context.Context, userID string, limit int) ([]map[string]any, error) {
|
||||||
filter := fmt.Sprintf("user_id='%s'", userID)
|
filter := fmt.Sprintf(`user_id="%s"`, userID)
|
||||||
items, err := s.pb.listAll(ctx, "notifications", filter, "-created")
|
items, err := s.pb.listAll(ctx, "notifications", filter, "-created")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -805,7 +805,7 @@ func (s *Store) DeleteNotification(ctx context.Context, id string) error {
|
|||||||
|
|
||||||
// ClearAllNotifications deletes all notifications for a user.
|
// ClearAllNotifications deletes all notifications for a user.
|
||||||
func (s *Store) ClearAllNotifications(ctx context.Context, userID string) error {
|
func (s *Store) ClearAllNotifications(ctx context.Context, userID string) error {
|
||||||
filter := fmt.Sprintf("user_id='%s'", userID)
|
filter := fmt.Sprintf(`user_id="%s"`, userID)
|
||||||
items, err := s.pb.listAll(ctx, "notifications", filter, "")
|
items, err := s.pb.listAll(ctx, "notifications", filter, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ClearAllNotifications list: %w", err)
|
return fmt.Errorf("ClearAllNotifications list: %w", err)
|
||||||
@@ -823,7 +823,7 @@ func (s *Store) ClearAllNotifications(ctx context.Context, userID string) error
|
|||||||
|
|
||||||
// MarkAllNotificationsRead marks all notifications for a user as read.
|
// MarkAllNotificationsRead marks all notifications for a user as read.
|
||||||
func (s *Store) MarkAllNotificationsRead(ctx context.Context, userID string) error {
|
func (s *Store) MarkAllNotificationsRead(ctx context.Context, userID string) error {
|
||||||
filter := fmt.Sprintf("user_id='%s'&&read=false", userID)
|
filter := fmt.Sprintf(`user_id="%s"&&read=false`, userID)
|
||||||
items, err := s.pb.listAll(ctx, "notifications", filter, "")
|
items, err := s.pb.listAll(ctx, "notifications", filter, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("MarkAllNotificationsRead list: %w", err)
|
return fmt.Errorf("MarkAllNotificationsRead list: %w", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user