- 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
8.0 KiB
8.0 KiB
Crussell
Nail salon booking platform — Go 1.25 backend + SvelteKit 5 frontend + Docker. Built for a UK sole-trader nail artist.
Features
- Three booking flows: self-service (customer), walk-in (admin), call-in (admin)
- Slot reservations: temporary holds prevent double-booking (4 TTL types)
- Guest accounts: disposable accounts for one-off bookings, GDPR-compliant anonymization
- Service eligibility: age requirements + patch test validation
- Deposit system: 3-strike rule for late cancellations, 24-hour threshold
- Square payments: in-person Terminal + online Web Payments SDK (dev mock + prod stub, build-tagged)
- Payment methods: Card (Square Terminal/online), cash, gift card — with saved cards, refunds, and webhook handling
- Payment types: deposit, full, partial, balance, tip — admin can override service prices during payment
- Scheduling: default hours, holiday overrides, time blockers
- Loyalty & discounts: stamp-based loyalty, time-based and milestone campaigns
- Portfolio gallery: S3/R2 storage with tag/category filtering
- CardDAV sync: profile photos synced to SabreDAV contacts
- Admin notifications: priority-sorted queue with bell icon,
/notificationspage, acknowledge flow - User notification preferences: per-channel toggles (email, SMS, browser) in account settings
- Enriched edit requests: side-by-side original vs proposed booking snapshots (time, services, prices, durations, user details) for admin review
- Admin reschedule modal: full reschedule UI on Today page with real-time available slot lookup, conflict detection, and one-click confirmation
- Time blockers UI: admin can create/manage one-off and recurring time blockers directly from the Admin dashboard, with overlap detection against existing bookings
- Today page enhancements: interactive daily calendar grid with visual time blockers, today stats summary, responsive layout improvements
- Auto-select availability: all booking flows (self-service, admin create, edit request) automatically select the first available date when data loads
- Shared time slot utilities: extracted common time slot generation, lunch protection, and formatting logic into a reusable module
- Shared formatting utilities:
formatDuration,formatDateTime,formatDate,formatTime,calculateAgeinlib/utils/format.ts - Referral code registration: new users can enter a 12-character referral code during registration; relationship recorded in
user_referralstable - Admin schedule page: Google Calendar-style week view at
/admin/schedulewith drag-scroll, booking details modal, and working hours overlays - BookingFlow welcome step: unauthenticated users see a welcome card (Step 0) encouraging login before guest checkout, with clear messaging about lost loyalty/discount benefits
- Admin login redirect: admins are redirected to
/todayafter login instead of the home page - Patch test duration on services:
patch_test_duration_hoursexposed on Service type; creating a service with duration > 0 auto-creates a patch test record created_by_nameon bookings: admin booking details now show who created the booking (admin name)- Exceptional scheduling fix: exceptional application queries expand start to Monday of the week, fixing single-day queries missing Monday-based
week_startrecords - Portfolio image upload limits: 20MB file size limit with visual feedback (red borders, error text), upload button disabled for oversized files
- NavBar enhancements: admin links reordered (Today/Schedule promoted), unread notification badge on mobile burger icon, backdrop overlay + slide transition for mobile menu
- TodayCalendar improvements: fetches working/available hours for full week range instead of single day, closing time indicator on timeline, skips lunch suggestion for short days (5h or less)
- Lunch protection refinement:
shouldApplyLunchProtection()skips lunch protection for days with 5 or fewer working hours formatDateISOutility: newformatDateISO(date)function inlib/utils/format.tsreturning YYYY-MM-DD format for API calls, later extracted to shared utilities- JWT revocation with JTI: every JWT carries a unique
jticlaim (UUID v4), in-memory revoked JTI tracking with 5-minute cleanup ticker,POST /api/logoutendpoint revokes current token, refresh handler revokes old JTI before issuing replacement - Portfolio image deletion fix:
extractKeycorrectly extracts full S3 key path from URLs instead of just the filename, preventing orphaned files in storage - Notes validation: all
Notes *stringfields across booking structs validated withmax=1000000tag (13 fields across 4 files) - CharCounter component: reusable grapheme counter using
Intl.Segmenter, shows counter only above 750K graphemes, color-coded (green <800K, yellow 800K-950K, red >950K), integrated into 6 booking/admin components - loginInProgress rate limiting: switched from
map[string]booltomap[string]time.Timewith 30-second staleness check, 20-entry cap (returns 429 when full), ticker goroutine cleans up stuck entries - Profile picture upload limit: 15MB client-side check before crop dialog in account page
- Portfolio image upload backend limit: separate
portfolioBodyLimit(20MB) applied to/imagesroute, distinct fromuploadBodyLimit(15MB) for profile pictures
Project Structure
Crussell/
├─ backend/ # Go 1.25 + chi router API
│ ├─ handlers/ # API handlers (auth, bookings, payments, scheduling, etc.)
│ │ ├─ payments/ # Square payment handlers (terminal, online, refunds, tips)
│ │ └─ webhooks/ # Square webhook handler
│ ├─ internal/ # Internal packages
│ │ └─ square/ # Square client (dev mock + prod stub, build-tagged)
│ └─ testutils/ # Test helpers (fixtures, testdb, JWT)
├─ frontend/ # SvelteKit 5 SPA (static build)
│ └─ src/lib/components/
│ ├─ payments/ # PaymentModal (admin), UserPaymentModal (user)
│ ├─ booking/ # BookingFlow (5-step wizard), DatePicker, TimeSlotPicker, TimeSlotList, SelectedTimeSummary
│ ├─ admin/ # BookingCreateModal, RescheduleModal, TimeBlockers, WeeklySchedule, HolidayHours, etc.
│ ├─ today/ # TodayCalendar (interactive grid), CurrentAppointment, PendingApprovals, TodayStats
│ └─ account/ # EditRequestModal, UserBookingModal
├─ frontend/src/lib/utils/ # Shared utilities (timeSlots.ts, format.ts)
├─ sabredav/ # PHP + Composer for DAV
├─ nginx/ # Nginx reverse-proxy for HTTP & HTTPS
├─ init-scripts/ # PostgreSQL init SQL
├─ compose.yml # Docker-Compose definition
├─ local-dev-2.sh # Dev helper using tmux (seeds DB with test data)
└─ README.md
Prerequisites
| Tool | Version |
|---|---|
| Docker & Docker Compose | >= 20.10 |
| Go | >= 1.22 |
| Node | >= 18 (npm) |
| tmux | >= 3.0 |
Getting Started
Docker Compose
cp .env.example .env
# Edit .env — set POSTGRES_*, JWT_SECRET_KEY
docker compose up --build -d
| Service | URL |
|---|---|
| Frontend | http://localhost |
| API | http://localhost/api |
| SabreDAV | http://localhost/dav |
Local Development
chmod +x local-dev-2.sh
./local-dev-2.sh
Launches a tmux session (crussell-dev) with 4 panes: psql console, Go dev server, Svelte dev server, and Rustfs logs. Seeds the database with 20 users, 12 services, 43 bookings, guest accounts, time blockers, and exceptional hours.
Default login (all password: password):
- Admin:
admin@example.com - User:
user@example.com
Building & Testing
# Backend
cd backend && go build -o bin/backend ./main.go
# Frontend
cd frontend && npm ci && npm run build
# Tests (446/449 passing, 3 skipped)
cd backend && go test -tags "test,dev" ./...
Full Documentation
For detailed user journeys, admin workflows, architecture, API reference, and database schema, see obsidian/Crussell/.