feat(auth,security,scheduling): JWT revocation, S3 fix, notes validation, docs, tests

- JWT revocation with JTI (UUID v4): in-memory tracking, POST /api/logout,
  refresh handler revokes old JTI, RequireAuth rejects revoked tokens
- Fix extractKey for S3 portfolio deletion: extracts full key path from URLs
  instead of just filename, preventing orphaned storage files
- Notes validation: max=1000000 on all 13 Notes fields across 4 booking structs
- CharCounter: grapheme-aware counter (Intl.Segmenter), threshold 750K,
  color-coded, integrated into 6 booking/admin components
- loginInProgress: timestamp-based tracking, 30s staleness, 20-entry cap (429),
  ticker cleanup for stuck entries
- Profile picture 15MB client-side limit, portfolio 20MB backend limit
- Exceptional scheduling: expand query start to Monday of week
- TodayCalendar: week-range fetching, closing time indicator, short-day lunch skip
- NavBar: link reorder, mobile burger badge, slide transition, backdrop
- ImageUpload: 20MB limit with visual feedback
- formatDateISO: shared YYYY-MM-DD utility, shouldApplyLunchProtection helper
- Update README.md and all Obsidian docs (Overview, Technical, Admin, Future Work)
- Add 28 new tests: JWT (11), auth handlers (7), portfolio extractKey (5),
  notes validation (5). go build + go vet clean with test,dev tags
This commit is contained in:
2026-06-03 11:17:41 +01:00
parent 169d7dc6e3
commit bffb984ebb
30 changed files with 1016 additions and 62 deletions
+6 -5
View File
@@ -477,11 +477,11 @@ The window shows the full booking details so you can review them before making y
## Notifications
The notifications page keeps you informed about everything happening with your bookings and customers. You'll find a bell icon in the top-right corner of the navigation bar — if there's a red dot on it, you have unread notifications.
The notifications page keeps you informed about everything happening with your bookings and customers. You'll find a bell icon in the top-right corner of the navigation bar — if there's a red dot on it, you have unread notifications. On mobile, the unread count also appears on the hamburger menu icon so you can see it at a glance when the menu is collapsed.
### How to Access
Click the **bell icon** in the top-right corner of the website to go to the Notifications page. The bell shows a number indicating how many unread notifications you have.
Click the **bell icon** in the top-right corner of the website (or the **notification badge** on the mobile hamburger menu) to go to the Notifications page. The bell shows a number indicating how many unread notifications you have.
### What You'll See
@@ -554,9 +554,10 @@ The portfolio is the salon's gallery of nail art photos that customers can brows
2. Find the image upload section
3. **Drag and drop** an image file, or click to select one from your device
4. A preview of the image appears so you can check it looks right
5. **Add tags** — type keywords that describe the image (for example, "french tip", "red", "summer", "glitter"). As you type, the system suggests tags that have been used before, so you can keep tags consistent
6. A confirmation window appears before the image is uploaded — this is your last chance to double-check
7. **Confirm** — the image is uploaded and appears in the portfolio gallery
5. **File size limit**: Images must be under 20MB. If you select a file larger than 20MB, the upload area gets a red border and shows an error message, and the upload button is disabled until you choose a smaller file
6. **Add tags** — type keywords that describe the image (for example, "french tip", "red", "summer", "glitter"). As you type, the system suggests tags that have been used before, so you can keep tags consistent
7. A confirmation window appears before the image is uploaded — this is your last chance to double-check
8. **Confirm** — the image is uploaded and appears in the portfolio gallery
### Tips for Tagging
@@ -1,4 +1,4 @@
**Last Updated:** May 2026 — Admin schedule page (weekly calendar view), referral code registration, BookingFlow welcome step for guests, patch_test_duration_hours on services, shared format utilities, created_by_name on bookings, admin login redirect, 446/449 tests passing (3 skipped)
**Last Updated:** June 2026 — JWT revocation with JTI, portfolio image upload limits (20MB frontend/backend), notes validation (max=1000000), CharCounter component, loginInProgress rate limiting cap, profile picture 15MB limit, formatDateISO utility, NavBar/TodayCalendar/lunch protection refinements
**Status:** Living backlog — add to this as gaps are discovered
---
+2 -1
View File
@@ -91,6 +91,7 @@ flowchart TD
- Email verification and password reset endpoints (backend ready, frontend not wired)
- Guest/disposable accounts for one-off bookings
- **Admin login redirect**: admins are redirected to `/today` after login instead of the home page
- **JWT revocation with JTI**: every JWT includes a unique `jti` claim (UUID v4) for revocation tracking. In-memory map of revoked JTIs with 5-minute cleanup ticker. `POST /api/logout` revokes the current token. Refresh handler revokes old JTI before issuing new token
### Booking System
- Three booking flows: self-service (customer), walk-in (admin), call-in (admin)
@@ -207,7 +208,7 @@ All flows integrate with holiday/exceptional hours and time blockers. All bookin
## Test Coverage
**446/449 tests passing** (3 skipped) across 12+ test packages. Comprehensive coverage of online payments (deposit, full, partial, balance), saved card operations, tip payments, terminal payments, refunds, idempotency, webhook handling, cash/gift card payments, enriched edit request workflows, GetBookingsByCreatedRange endpoint, scheduling exceptional hours, and referral code validation.
**446/449 tests passing** (3 skipped) across 12+ test packages. Comprehensive coverage of online payments (deposit, full, partial, balance), saved card operations, tip payments, terminal payments, refunds, idempotency, webhook handling, cash/gift card payments, enriched edit request workflows, GetBookingsByCreatedRange endpoint, scheduling exceptional hours, referral code validation, and JWT revocation via JTI logout.
| Package | Coverage Area |
|---------|--------------|
+23 -3
View File
@@ -63,7 +63,7 @@ Backend (:8080)
| Middleware | Purpose |
|------------|---------|
| `RequireAuth` | Validates JWT, adds user_id and user_role to context |
| `RequireAuth` | Validates JWT, extracts `jti` claim, adds user_id and user_role to context; rejects revoked JTIs |
| `OptionalAuth` | Extracts user info if token present, passes through otherwise |
| `RequireAdmin` | Allows admin role only |
| `RequireVerified` | Allows verified_email or admin |
@@ -178,12 +178,15 @@ src/lib/components/
- `getDayWithOrdinal()` — formats a CalendarDate with ordinal suffix (e.g., "January 15th")
- Types: `DayHours`, `DayAvailability` — shared type definitions for working/available hours data
- **`CharCounter` component** (`lib/components/ui/CharCounter.svelte`): Reusable grapheme counter using `Intl.Segmenter` for correct character boundary detection (handles emoji, multi-byte chars). Hidden below 750K graphemes, color-coded above: green (<800K), yellow (800K-950K), red (>950K). Integrated into 6 booking/admin components for notes fields.
- **`lib/utils/format.ts`**: Shared formatting utilities for consistent display across the app
- `formatDuration(minutes)` — converts minutes to human-readable string (e.g., 90 → "1h 30m")
- `formatDateTime(date)` — formats to "Weekday, Month Day at HH:MM AM/PM"
- `formatDate(date)` — formats to "Weekday, Month Day" (no time)
- `formatTime(date)` — formats to "HH:MM AM/PM"
- `calculateAge(dateOfBirth)` — calculates age in years from DOB string
- `formatDateISO(date)` — formats a Date object to YYYY-MM-DD string for API calls
---
@@ -217,7 +220,8 @@ src/lib/components/
| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/refresh-token` | Refresh JWT (role-change detection) |
| POST | `/api/logout` | Revoke current JWT token |
| POST | `/api/refresh-token` | Refresh JWT (role-change detection, revokes old JTI) |
| GET | `/api/user/profile` | Get current user profile |
| PUT | `/api/user/profile` | Update profile |
| POST | `/api/user/profile-picture` | Upload profile picture (cropper) |
@@ -653,6 +657,8 @@ type EnrichedEditRequest struct {
**Frontend:** `buildLunchProtection()` in `lib/utils/timeSlots.ts` consolidates lunch protection logic that was previously duplicated across BookingFlow, BookingCreateModal, and EditRequestModal. All booking flows now use the shared utility.
**`shouldApplyLunchProtection()`:** A new helper that skips lunch protection entirely for short working days (5 hours or less). This prevents false lunch-break warnings on half-days or days with abbreviated hours.
---
### Referral Code System
@@ -803,7 +809,7 @@ go test -tags "test,dev" -v -p 1 -count=2 ./... # Run twice for flaky detection
### Test Coverage
**446/449 tests passing** (3 skipped) across 12+ packages. Comprehensive coverage of online payments (deposit, full, partial, balance), saved card operations, tip payments, terminal payments, refunds, idempotency, webhook handling, and cash/gift card payments, GetBookingsByCreatedRange endpoint, scheduling exceptional hours validation, and referral code registration.
**446/449 tests passing** (3 skipped) across 12+ packages. Comprehensive coverage of online payments (deposit, full, partial, balance), saved card operations, tip payments, terminal payments, refunds, idempotency, webhook handling, and cash/gift card payments, GetBookingsByCreatedRange endpoint, scheduling exceptional hours validation, referral code registration, and JWT revocation via JTI logout.
- `handlers/auth` — Authentication (login, register, referral code validation, refresh, verification)
- `handlers/bookings` — User booking flow, guest bookings, reservations, edit requests, discounts, closing hours validation, active booking limits, GetBookingsByCreatedRange
- `handlers/payments` — Square payments (terminal, online, refunds, tips, saved cards)
@@ -834,6 +840,7 @@ go test -tags "test,dev" -v -p 1 -count=2 ./... # Run twice for flaky detection
- **Buckets**: `crussell` (portfolio), `crussell-profile-pics` (profile pictures)
- **Image formats**: AVIF full-size (0.72 quality, 1500px max), WebP thumbnails (250x250)
- **Security**: EXIF/GPS metadata stripped on upload via `imaging` library
- **extractKey fix**: Portfolio image delete now correctly extracts the full S3 key path (e.g., `portfolio/1234567890.jpg`) from URLs instead of just the filename. This prevents orphaned files in storage
### Square Payment Integration
@@ -879,6 +886,10 @@ go test -tags "test,dev" -v -p 1 -count=2 ./... # Run twice for flaky detection
- **Secret**: `JWT_SECRET_KEY` environment variable (required, checked in `init()`)
- **Refresh**: Auto-refresh via `POST /api/refresh-token`
- **Role-change detection**: Forces re-login if role changed
- **JWT ID (JTI)**: Every JWT includes a unique `jti` claim (UUID v4) for revocation tracking
- **Revocation**: In-memory `map[string]time.Time` tracks revoked JTIs. A background ticker runs every 5 minutes to purge expired entries
- **Logout**: `POST /api/logout` revokes the current JWT token by adding its JTI to the revocation map
- **Refresh revocation**: When a new token is issued via refresh, the old JTI is added to the revocation map
### Rate Limiting
@@ -894,6 +905,12 @@ go test -tags "test,dev" -v -p 1 -count=2 ./... # Run twice for flaky detection
**IP extraction**: CF-Connecting-IP → X-Real-IP → X-Forwarded-For → RemoteAddr
**loginInProgress rate limiting:**
- `loginInProgress` is a `map[string]time.Time` tracking in-progress login attempts
- 30-second staleness check: entries older than 30s are treated as expired
- 20-entry size cap: returns 429 "too many login attempts" when full
- A ticker goroutine periodically cleans up stuck/expired entries
### Security Headers
- `X-Content-Type-Options: nosniff`
@@ -910,11 +927,14 @@ go test -tags "test,dev" -v -p 1 -count=2 ./... # Run twice for flaky detection
- **Age**: Must be 16+ years
- **Services**: name (100), price (>0), duration (1-480), patch test (0-168), age (0-100)
- **Portfolio**: tags/filters (256 char max), filter category validation, image ID pattern security
- **Notes**: all `Notes *string` fields across booking structs (13 fields across 4 files) have `validate:"omitempty,max=1000000"` tag
### Image Security
- EXIF/GPS metadata stripped on all uploads via `imaging` library
- Profile pictures stored in separate bucket from portfolio images
- **Profile picture upload limit**: 15MB client-side check before crop dialog in account page
- **Portfolio image upload limit**: 20MB backend limit via `portfolioBodyLimit` constant applied to `/images` route, distinct from `uploadBodyLimit` (15MB) for profile pictures. Frontend also enforces 20MB with visual feedback (red borders, error text) and disables upload button for oversized files
---