Admin for the menu and enquiries, plus gallery fixes

Admin
- /api/admin: products CRUD, the enquiry inbox, and presigned photo upload straight to the
  bucket so images never pass through the app. Gated by platform.security.authenticated-paths
  = /api/admin/**, so any signed-in Authentik user is staff — the alternative is a role model
  a two-person bakery would never maintain.
- /api/me is deliberately PUBLIC. The SPA asks on every page load, and requiring a login
  would bounce every anonymous visitor to Authentik just to read the menu.
- /admin screens: product list with edit and remove, an editor with drag-free photo
  reordering and upload, and an enquiry inbox that flags anything the relay refused.

Gallery
- swipe on touch devices, which the react-awesome-slider it replaced had and this did not,
  plus arrow keys and position dots — with swipe there is otherwise nothing to say a card
  holds more than one photo. Vertical drags are ignored so page scrolling still works.
- @BatchSize on the photo collection: the products page loaded the whole catalogue and
  Hibernate issued a query per product for its images, forty-odd round trips for a page
  that needs two.

Three things the tests caught, none of which are obvious:
- Adding the storage starter broke every existing test. It activates on a default endpoint,
  so an S3 client is built even in tests and dies on blank keys.
- MockMvc's webAppContextSetup leaves the security filter chain OUT, so the first version of
  the security test passed 200s and proved the opposite of what it claimed. It needs
  .apply(springSecurity()).
- Turning on the security starter turns on CSRF — for the PUBLIC contact form too, which
  then 403s. The SPA now reads the XSRF-TOKEN cookie and sends X-XSRF-TOKEN, and there is a
  test asserting the form is rejected without it.
This commit is contained in:
2026-07-23 11:58:21 -05:00
parent 27821cdb90
commit d2c62f35ed
18 changed files with 1040 additions and 15 deletions
+8
View File
@@ -7,6 +7,9 @@ import ProductsPage from '@/pages/Products';
import HistoryPage from '@/pages/History';
import ContactPage from '@/pages/Contact';
import NotFoundPage from '@/pages/NotFound';
import AdminPage from '@/pages/admin/AdminPage';
import ProductEditorPage from '@/pages/admin/ProductEditorPage';
import { AuthProvider } from '@/lib/auth';
/**
* Client-side navigation keeps the previous scroll position, which lands you halfway down a page you
@@ -23,6 +26,7 @@ const ScrollToTop = () => {
};
const App = () => (
<AuthProvider>
<div className="min-h-screen bg-bakery-50 flex flex-col">
<ScrollToTop />
<Header />
@@ -32,11 +36,15 @@ const App = () => (
<Route path="/products" element={<ProductsPage />} />
<Route path="/history" element={<HistoryPage />} />
<Route path="/contact" element={<ContactPage />} />
<Route path="/admin" element={<AdminPage />} />
<Route path="/admin/products/new" element={<ProductEditorPage />} />
<Route path="/admin/products/:id" element={<ProductEditorPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</main>
<Footer />
</div>
</AuthProvider>
);
export default App;