- 🔴 Event ownership: GET /event/:eventId and DELETE /reorder/:eventId now use
requireEventOwnership; POST /reorder (event_id in body) gets the equivalent
inline check (super_admin bypasses; others limited to owned/ownerless events).
New test covers a settings.edit-holding non-super_admin blocked (403) on all
three per-event routes.
- 🔴 Migration renumber: 158→159, 159→160 (upstream #788 already took 158);
headers + the test's require path updated.
- 🟢 Nits: stale inline "Drag the arrows" fallback → "Use the arrows" (matches
en.json; control is click-only); invalid bg-accent-dark/150 → bg-accent-dark.
Order a gallery's categories in the flow of the day instead of A–Z. Two layers,
resolved per event: per-event override > global default > name.
- migration 158: photo_categories.display_order (global default), backfilled
from the current alphabetical order so existing galleries don't reshuffle.
- migration 159: event_category_order (event_id, category_id, position) — the
per-event override; no backfill, every event starts on the default.
- utils/categoryOrder: shared resolution used by the admin event view and the
public gallery; fails safe to the global default if the table is absent.
- adminCategories: POST /reorder sets a per-event override (globals +
event-specific, interleaved); DELETE /reorder/:eventId resets; POST
/reorder-global sets the global default. Ordering endpoints + create append.
- gallery renders the resolved order.
- Settings → Photo Categories reorders the global default; an event's Categories
tab reorders that gallery (one combined list + Reset to default). Up/down
buttons — no drag-and-drop dependency.
- en/de strings.
Adds an `allow_downloads` boolean to `photo_categories` so admins can
have different download policies per category — e.g. preview categories
public, originals client-only. AND's with the event-level `allow_downloads`,
so disabling at either level blocks downloads for that category's photos.
Defaults to true so categories created before migration 135 keep working
without admin intervention.
Credit: 8digit/picpeak@928164b + @751ec75.
### Backend
- **Migration 135**: additive `allow_downloads BOOLEAN NOT NULL DEFAULT true`
on `photo_categories`, hasColumn-guarded + sane down.
- **`adminCategories.js`**: PUT /:id accepts optional `allow_downloads` patch.
- **`gallery.js`**:
- `GET /:slug/photos` returns `allow_downloads` per category AND
`category_allow_downloads` per photo.
- `GET /:slug/download/:photoId` returns 403 when the photo's category
disables downloads.
- `GET /:slug/download-all` LEFT JOINs `photo_categories` and filters
`whereNull(category_id) OR allow_downloads=true OR allow_downloads IS NULL`.
The null check covers pre-migration-135 rows during the upgrade window.
- `POST /:slug/download-selected` same filter pattern.
### Frontend
- **`categories.service.ts`**: `updateCategory()` gains an optional `patch`
argument carrying `{ allow_downloads }`. PhotoCategory interface gains the
optional field.
- **`EventCategoryManager.tsx`**: new toggle button next to the delete X.
Green DownloadCloud icon when downloads are on, plain Download icon when
off. Click toggles via the new mutation; toast confirms.
- **`PhotoLightbox.tsx`**: `photoAllowsDownload = allowDownloads && currentPhoto?.category_allow_downloads !== false`. Hides the download button +
blocks the 'D' keyboard shortcut + early-returns from handleDownload.
- **Types**: Photo interface gains `category_allow_downloads`.
- **i18n**: 5 new EN + DE entries for the toggle button toast + tooltip.
No global-category surface change yet — global categories don't currently
have a UI for the toggle. Admins can still flip the column directly via SQL
or via a future global-categories editor.
### Test plan
- [x] Backend syntax + TS check clean
- [x] ESLint: no new warnings
- [ ] Manual: admin → event detail → categories panel → click DownloadCloud
icon → category flips, toast confirms
- [ ] Manual: gallery (guest) → photo in disabled category → lightbox shows
no download button, 'D' shortcut is a no-op
- [ ] Manual: download-all on a gallery with one disabled category →
ZIP excludes that category's photos
- [ ] Manual: download-selected including a disabled-category photo → 404
(filtered out) and the response carries only the allowed selection
- [ ] Manual: pre-migration-135 category (legacy row with NULL allow_downloads)
→ downloads still work (defaults true via fallback)
Accented chars (ã, ç, é, etc.) were silently dropped by the slug
regex because \w only matches ASCII. NFD decomposition + combining
mark removal converts them to ASCII equivalents instead.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Wire up the hero_photo_id column on photo_categories that was added in
the migration but never connected. Backend routes now accept and persist
hero_photo_id on category create/update, a dedicated PUT /:id/hero
endpoint is added, and the gallery API returns hero_photo_id for each
category. Frontend EventCategoryManager shows a clickable thumbnail per
category that opens a photo picker modal. Includes EN/DE i18n keys.
Original: feat: enhance security logging and ensure rate limit blocks are properly tracked
- Add comprehensive logging for rate limit blocks with full request details
- IP address (with proper proxy detection), user agent, headers, timestamps
- Rate limit info (current count, limit, remaining, reset time)
- Separate tracking for auth vs general endpoints
- Enhance authentication failure logging
- JWT validation failures with detailed error info
- Admin auth attempts without token
- Failed token validation with user context
- All events include IP, path, method, user agent
- Improve Winston logger configuration for production
- Add automatic log rotation (10MB errors, 50MB combined)
- Create separate security.log for auth/rate limit events
- Ensure logs directory exists automatically
- Add structured JSON format for log aggregation
- Support container logging with LOG_TO_CONSOLE env var
- Create comprehensive documentation
- Security logging guide with examples
- Monitoring recommendations
- Configuration reference
- Add test script to verify logging functionality
All rate limit settings remain configurable via admin panel:
- Window duration, max requests, auth limits
- Skip authenticated requests option
- Public endpoints only option
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <[email protected]>