diff --git a/server/router/api/v1/memo_service.go b/server/router/api/v1/memo_service.go index 3c9aa1ecd..b1d881e2c 100644 --- a/server/router/api/v1/memo_service.go +++ b/server/router/api/v1/memo_service.go @@ -647,6 +647,12 @@ func (s *APIV1Service) CreateMemoComment(ctx context.Context, request *v1pb.Crea slog.Warn("Failed to dispatch memo comment created webhook", slog.Any("err", err)) } + // Broadcast live refresh event for the parent memo so subscribers see the new comment. + s.SSEHub.Broadcast(&SSEEvent{ + Type: SSEEventMemoCommentCreated, + Name: request.Name, + }) + return memoComment, nil } diff --git a/server/router/api/v1/sse_handler.go b/server/router/api/v1/sse_handler.go index 09379804c..07b36d01c 100644 --- a/server/router/api/v1/sse_handler.go +++ b/server/router/api/v1/sse_handler.go @@ -26,18 +26,10 @@ func RegisterSSERoutes(echoServer *echo.Echo, hub *SSEHub, storeInstance *store. } // handleSSE handles the SSE connection for live memo refresh. -// Authentication is done via Bearer token in the Authorization header, -// or via the "token" query parameter (for EventSource which cannot set headers). +// Authentication is done via Bearer token in the Authorization header. func handleSSE(c *echo.Context, hub *SSEHub, authenticator *auth.Authenticator) error { // Authenticate the request. authHeader := c.Request().Header.Get("Authorization") - if authHeader == "" { - // Fall back to query parameter for native EventSource support. - if token := c.QueryParam("token"); token != "" { - authHeader = "Bearer " + token - } - } - result := authenticator.Authenticate(c.Request().Context(), authHeader) if result == nil { return c.JSON(http.StatusUnauthorized, map[string]string{"error": "authentication required"}) diff --git a/server/router/api/v1/sse_hub.go b/server/router/api/v1/sse_hub.go index 245ac7b56..1fecad8af 100644 --- a/server/router/api/v1/sse_hub.go +++ b/server/router/api/v1/sse_hub.go @@ -10,11 +10,12 @@ import ( type SSEEventType string const ( - SSEEventMemoCreated SSEEventType = "memo.created" - SSEEventMemoUpdated SSEEventType = "memo.updated" - SSEEventMemoDeleted SSEEventType = "memo.deleted" - SSEEventReactionUpserted SSEEventType = "reaction.upserted" - SSEEventReactionDeleted SSEEventType = "reaction.deleted" + SSEEventMemoCreated SSEEventType = "memo.created" + SSEEventMemoUpdated SSEEventType = "memo.updated" + SSEEventMemoDeleted SSEEventType = "memo.deleted" + SSEEventMemoCommentCreated SSEEventType = "memo.comment.created" + SSEEventReactionUpserted SSEEventType = "reaction.upserted" + SSEEventReactionDeleted SSEEventType = "reaction.deleted" ) // SSEEvent represents a change event sent to SSE clients. diff --git a/server/router/api/v1/test/sse_handler_test.go b/server/router/api/v1/test/sse_handler_test.go index d182b279a..c06f9e3fd 100644 --- a/server/router/api/v1/test/sse_handler_test.go +++ b/server/router/api/v1/test/sse_handler_test.go @@ -69,18 +69,10 @@ func TestSSEHandler_Authentication(t *testing.T) { require.Equal(t, "text/event-stream", rec.Header().Get("Content-Type")) }) - t.Run("token in query param returns 200", func(t *testing.T) { - reqCtx, cancel := context.WithCancel(context.Background()) - defer cancel() - req := httptest.NewRequest(http.MethodGet, "/api/v1/sse?token="+token, nil).WithContext(reqCtx) + t.Run("token in query param returns 401", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/v1/sse?token="+token, nil) rec := httptest.NewRecorder() - done := make(chan struct{}) - go func() { - defer close(done) - e.ServeHTTP(rec, req) - }() - cancel() - <-done - require.Equal(t, http.StatusOK, rec.Code) + e.ServeHTTP(rec, req) + require.Equal(t, http.StatusUnauthorized, rec.Code) }) } diff --git a/web/src/components/Navigation.tsx b/web/src/components/Navigation.tsx index faf07c55b..484f0837b 100644 --- a/web/src/components/Navigation.tsx +++ b/web/src/components/Navigation.tsx @@ -8,7 +8,6 @@ import { Routes } from "@/router"; import { UserNotification_Status } from "@/types/proto/api/v1/user_service_pb"; import { useTranslate } from "@/utils/i18n"; import MemosLogo from "./MemosLogo"; -import SSEStatusIndicator from "./SSEStatusIndicator"; import UserMenu from "./UserMenu"; interface NavLinkItem { @@ -115,10 +114,7 @@ const Navigation = (props: Props) => { ))} {currentUser && ( -