diff --git a/api/handlers/staticimages/staticImagesHandler.go b/api/handlers/staticimages/staticImagesHandler.go index c26cbba..1b79394 100644 --- a/api/handlers/staticimages/staticImagesHandler.go +++ b/api/handlers/staticimages/staticImagesHandler.go @@ -12,7 +12,7 @@ import ( "github.com/go-chi/chi/v5" ) -const cacheControl = "public, max-age=31536000, immutable" +const cacheControl = "public, no-cache" const svgContentSecurityPolicy = "default-src 'none'; style-src 'unsafe-inline'; sandbox" const fallbackContentType = "application/octet-stream" diff --git a/docs/design/screens/articles.md b/docs/design/screens/articles.md index f64dd4e..90b983a 100644 --- a/docs/design/screens/articles.md +++ b/docs/design/screens/articles.md @@ -88,7 +88,7 @@ Rows update when the server responds, not optimistically. Search, tab and sort a - Breadcrumbs: Articles (mobile title "Articles") - Modal: **Delete this article?** - - text ""Winter checklist for timber windows" and its 3 images will be deleted. This can't be undone." + - text ""Winter checklist for timber windows" will be deleted. Its images stay in your library. This can't be undone." - cancel "Cancel" - confirm "Delete article" (danger) @@ -98,7 +98,7 @@ Rows update when the server responds, not optimistically. Search, tab and sort a - Breadcrumbs: Articles (mobile title "Articles") - Modal: **Delete this article?** - - text ""Winter checklist for timber windows" and its 3 images will be deleted. This can't be undone." + - text ""Winter checklist for timber windows" will be deleted. Its images stay in your library. This can't be undone." - cancel "Cancel" - confirm "Deleting…" (danger), loading, not dismissible @@ -108,7 +108,7 @@ Rows update when the server responds, not optimistically. Search, tab and sort a - Breadcrumbs: Articles (mobile title "Articles") - Modal: **Delete this article?** - - text ""Winter checklist for timber windows" and its 3 images will be deleted. This can't be undone." + - text ""Winter checklist for timber windows" will be deleted. Its images stay in your library. This can't be undone." - Alert: "Couldn't delete the article, so it's still there. Try again in a moment." - cancel "Cancel" - confirm "Delete article" (danger) @@ -118,5 +118,5 @@ Rows update when the server responds, not optimistically. Search, tab and sort a **Trigger:** 204: the row is removed and focus moves to the next row. This is also where a delete from the editor ends up. - Breadcrumbs: Articles (mobile title "Articles") -- Toast (success, circle-check): **Article deleted** ""Winter checklist for timber windows" and its images are gone." +- Toast (success, circle-check): **Article deleted** ""Winter checklist for timber windows" is gone." - Tab counts (All, Online, Drafts): 5, 4, 1 diff --git a/docs/design/screens/editor.md b/docs/design/screens/editor.md index d5336e0..585ce08 100644 --- a/docs/design/screens/editor.md +++ b/docs/design/screens/editor.md @@ -183,7 +183,7 @@ On mobile, Save and Publish sit in a sticky bottom bar and the details panel ope - Breadcrumbs: Articles › How we price a bathroom renovation (mobile title "Edit article") - Modal: **Delete this article?** - - text ""How we price a bathroom renovation" and its 3 images will be deleted. This can't be undone." + - text ""How we price a bathroom renovation" will be deleted. Its images stay in your library. This can't be undone." - cancel "Cancel" - confirm "Delete article" (danger) - Editor: status "draft", save "saved" diff --git a/features/articles_list.feature b/features/articles_list.feature index 680c740..ff5b891 100644 --- a/features/articles_list.feature +++ b/features/articles_list.feature @@ -59,9 +59,9 @@ Feature: Articles list screen remaining articles, and raises an info toast (icon info, id "article-not-found-{id}"): title "That article no longer exists" text "It was deleted somewhere else, so we removed it from the list." - And "Delete…" opens ConfirmDialog: title "Delete this article?" text "\"{title}\" and its - {n} images will be deleted. This can't be undone." (a components.md convention: the row's - title plus its image count, "and its image" for exactly 1 and no image clause at all for 0), + And "Delete…" opens ConfirmDialog: title "Delete this article?" text "\"{title}\" will be + deleted. Its images stay in your library. This can't be undone." (the second sentence is + present only when the article body holds at least one image), "Cancel" and "Delete article" (danger); while the DELETE request is in flight the dialog cannot be dismissed by Cancel, Esc or a backdrop click, both buttons are disabled, the confirm button reads "Deleting…" with a loading indicator, and aria-busy is set @@ -72,7 +72,7 @@ Feature: Articles list screen And a successful delete (204) closes the dialog, removes the row, moves focus to the next row (or the page heading if the deleted row was last), recomputes the tab counts, and raises a success toast (icon circle-check, id "article-deleted-{id}"): title "Article deleted" text - "\"{title}\" and its images are gone."; this is also where a delete initiated from the editor + "\"{title}\" is gone."; this is also where a delete initiated from the editor ends up And the list, sorted by last updated with the most recently updated article first, holds these 6 articles by default: id 1 "How we price a bathroom renovation" (online, updated "2 @@ -231,7 +231,7 @@ Feature: Articles list screen Given the Default state has loaded, and id 4 "Winter checklist for timber windows" has 3 images When the user chooses "Delete…" from id 4's row menu Then ConfirmDialog opens: title "Delete this article?" text "\"Winter checklist for timber - windows\" and its 3 images will be deleted. This can't be undone.", "Cancel" and "Delete + windows\" will be deleted. Its images stay in your library. This can't be undone.", "Cancel" and "Delete article" (danger) Scenario: Delete in progress — the dialog cannot be dismissed while the request is in flight @@ -256,4 +256,4 @@ Feature: Articles list screen Then the dialog closes, id 4's row is removed from the list, and focus moves to the next row And the tabs read "All 5" "Online 4" "Drafts 1" And a success toast appears: icon circle-check, id "article-deleted-4", title "Article - deleted", text "\"Winter checklist for timber windows\" and its images are gone." + deleted", text "\"Winter checklist for timber windows\" is gone." diff --git a/features/articles_list.qa.md b/features/articles_list.qa.md index 68859f0..3096cbf 100644 --- a/features/articles_list.qa.md +++ b/features/articles_list.qa.md @@ -114,7 +114,7 @@ as listed), unless a scenario's Examples row substitutes a smaller fixture. 18. **Deleted** — continuing from 16 (a fresh attempt), resolve the DELETE 204; assert the dialog closes, id 4's row is removed, focus moves to the next row; assert tabs read "All 5" "Online 4" "Drafts 1"; assert a toast with id "article-deleted-4", icon circle-check, title "Article - deleted", text "\"Winter checklist for timber windows\" and its images are gone." + deleted", text "\"Winter checklist for timber windows\" is gone." ## Visual (manual verification — desktop and mobile, light and dark) diff --git a/features/serve_uploaded_images.feature b/features/serve_uploaded_images.feature index 0410176..4c8fea9 100644 --- a/features/serve_uploaded_images.feature +++ b/features/serve_uploaded_images.feature @@ -23,9 +23,11 @@ Feature: Serving uploaded images as static files "application/octet-stream" And every response from this handler, success or 404, sets "X-Content-Type-Options: nosniff" - And every successful response sets a long-lived, public Cache-Control - header, e.g. "public, max-age=31536000, immutable", because a stored - filename is a UUID and its bytes never change + And every successful response sets the Cache-Control header + "public, no-cache", so a browser may store the file but must revalidate + it before reusing it; a stored filename is a UUID and its bytes never + change, but the file can be deleted from the media library, and a + long-lived cache would keep showing it And an "image/svg+xml" response additionally sets "Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox", so a script embedded in an uploaded SVG @@ -42,7 +44,7 @@ Feature: Serving uploaded images as static files cookie Then the response status is 200 And the response Content-Type is - And the response Cache-Control is "public, max-age=31536000, immutable" + And the response Cache-Control is "public, no-cache" And the response X-Content-Type-Options is "nosniff" And the response has no Content-Security-Policy header And the response body is that file's exact content @@ -61,7 +63,7 @@ Feature: Serving uploaded images as static files with no jwt cookie Then the response status is 200 And the response Content-Type is "image/svg+xml" - And the response Cache-Control is "public, max-age=31536000, immutable" + And the response Cache-Control is "public, no-cache" And the response X-Content-Type-Options is "nosniff" And the response Content-Security-Policy is "default-src 'none'; style-src 'unsafe-inline'; sandbox" @@ -128,7 +130,7 @@ Feature: Serving uploaded images as static files with no jwt cookie Then the response status is 200 And the response Content-Type is "image/webp" - And the response Cache-Control is "public, max-age=31536000, immutable" + And the response Cache-Control is "public, no-cache" And the response X-Content-Type-Options is "nosniff" And the response body is empty diff --git a/features/serve_uploaded_images.qa.md b/features/serve_uploaded_images.qa.md index 79799b0..e4932b9 100644 --- a/features/serve_uploaded_images.qa.md +++ b/features/serve_uploaded_images.qa.md @@ -29,7 +29,7 @@ Package: `test` (black-box, new file `test/serve_uploaded_images_test.go`). non-empty byte content. For each, `httptest.NewRequest("GET", "/static/uploadedImages/"+name, nil)`, no cookie. Assert status `200`; `Content-Type` equals the expected value; `Cache-Control` equals - `"public, max-age=31536000, immutable"`; `X-Content-Type-Options` equals + `"public, no-cache"`; `X-Content-Type-Options` equals `"nosniff"`; `Content-Security-Policy` header is absent; body bytes equal what was written. 7. Write a `.svg` file whose content contains an inline `onload` handler diff --git a/test/serve_uploaded_images_test.go b/test/serve_uploaded_images_test.go index cccb299..4267a71 100644 --- a/test/serve_uploaded_images_test.go +++ b/test/serve_uploaded_images_test.go @@ -64,7 +64,7 @@ func TestServeUploadedImagesHttp(t *testing.T) { if got := rec.Header().Get("Content-Type"); got != example.contentType { t.Fatalf("expected Content-Type %q, got %q", example.contentType, got) } - if got := rec.Header().Get("Cache-Control"); got != "public, max-age=31536000, immutable" { + if got := rec.Header().Get("Cache-Control"); got != "public, no-cache" { t.Fatalf("unexpected Cache-Control: %q", got) } if got := rec.Header().Get("X-Content-Type-Options"); got != "nosniff" { @@ -96,7 +96,7 @@ func TestServeUploadedImagesHttp(t *testing.T) { if got := rec.Header().Get("Content-Type"); got != "image/svg+xml" { t.Fatalf("expected Content-Type image/svg+xml, got %q", got) } - if got := rec.Header().Get("Cache-Control"); got != "public, max-age=31536000, immutable" { + if got := rec.Header().Get("Cache-Control"); got != "public, no-cache" { t.Fatalf("unexpected Cache-Control: %q", got) } if got := rec.Header().Get("X-Content-Type-Options"); got != "nosniff" { @@ -237,7 +237,7 @@ func TestServeUploadedImagesHttp(t *testing.T) { if got := rec.Header().Get("Content-Type"); got != "image/webp" { t.Fatalf("expected Content-Type image/webp, got %q", got) } - if got := rec.Header().Get("Cache-Control"); got != "public, max-age=31536000, immutable" { + if got := rec.Header().Get("Cache-Control"); got != "public, no-cache" { t.Fatalf("unexpected Cache-Control: %q", got) } if got := rec.Header().Get("X-Content-Type-Options"); got != "nosniff" { diff --git a/web/src/components/ArticleEditorImageNode.vue b/web/src/components/ArticleEditorImageNode.vue index 3e38f76..d921833 100644 --- a/web/src/components/ArticleEditorImageNode.vue +++ b/web/src/components/ArticleEditorImageNode.vue @@ -1,7 +1,8 @@