Crussell

Crussell is a fullstack application that powers a nailbar / salon booking service. The repository is split into a Go backend and a SvelteKit frontend, both of which are containerised with Docker. A lightweight SabreDAV instance is also exposed so that the salon can offer WebDAV access to clients.

📦 Project Structure

Crussell/
├─ backend/            # Go 1.25 + chi router API
├─ frontend/           # SvelteKit 5 SPA (static build)
├─ sabredav/           # PHP + Composer for DAV
├─ nginx/              # Nginx reverseproxy for HTTP & HTTPS
├─ init-scripts/       # PostgreSQL init SQL
├─ compose.yml         # DockerCompose definition
├─ local-dev-2.sh      # Development helper using tmux (seeded with 8 services incl. 2 with patch tests)
└─ README.md

⚙️ Prerequisites

Tool Version
Docker & DockerCompose ≥ 20.10
Go ≥ 1.22
Node ≥ 18 (npm)
tmux ≥ 3.0

Tip

: If you already have Docker Desktop or Docker Engine installed, you are good to go.

📥 Getting Started

# Clone the repository
git clone http://git.popertots.com/popertots/Crussell.git
cd Crussell

# Copy the example environment file and edit it
cp .env.example .env
# Open .env and provide values for POSTGRES_*, JWT_SECRET_KEY, etc.

DockerCompose

The simplest way to bring the whole stack up is with DockerCompose.

docker compose up --build -d

postgres PostgreSQL 17 backend Go API (exposed on :8080) sabredav PHPbased WebDAV (served by Nginx) nginx Reverseproxy (HTTP on :80 and HTTPS on :443)

After the containers are running, the frontend is reachable at http://localhost. The API is available at http://localhost/api. SabreDAV can be accessed via http://localhost/dav.

Development with local-dev-2.sh

For a more interactive dev experience the repository ships a small helper script that launches Docker, starts a tmux session with four panes (PostgreSQL console, Go dev server, Svelte dev server, Rustfs logs) and seeds the database with an admin, regular users, and sample services including patch test services.

chmod +x local-dev-2.sh
./local-dev-2.sh

The script performs the following steps:

  1. Docker checks starts Docker if it isn't already running.
  2. PostgreSQL reset removes the old volume and starts a fresh container.
  3. tmux session creates crussell-dev with panes:
    • psql console
    • Go server (go run -tags dev ./main.go)
    • Svelte dev server (npm run dev -- --host)
    • Rustfs logs
  4. Seeding creates admin (admin@example.com), regular users (user@example.com), 8 services (6 standard + 2 requiring patch tests), bookings, exceptional hours.

Note

: The script uses a temporary shell script to perform the HTTP calls, so no external tooling like jq is required.

🔧 Building & Testing

Backend

cd backend
# Build the binary
go build -o bin/backend ./main.go

The binary is then copied into the Docker image via the Dockerfile.

Frontend

cd frontend
npm ci
npm run build   # Production build (static)
npm run dev     # Development server

SabreDAV

SabreDAV is bundled with PHPFPM and Composer. The Docker image installs dependencies automatically during the container startup.

🧪 Testing

Test Infrastructure

Crussell has a comprehensive Go testing infrastructure located in backend/testutils/:

Component File Description
Database testutils/testdb/testdb.go PostgreSQL test pool, migrations, table truncation
HTTP Helpers testutils/helpers.go Request builders, auth helpers, assertions
JWT testutils/jwt/jwt.go Test token generation for users/admins
HTTP Client testutils/httptest/client.go REST client wrapper with auth support
Fixtures testutils/fixtures/fixtures.go Factory functions for test data
Validators internal/validators/validators.go ID validation utilities

Test Files

The project includes 16 test files covering all major handlers:

backend/
├── bookings.test              # Main booking integration tests
├── portfolio.test             # Portfolio system tests
├── handlers/
│   ├── admin/
│   │   ├── bookings_test.go  # Admin booking management
│   │   ├── today_test.go     # Today's view
│   │   ├── users_test.go     # User management
│   │   └── services_test.go  # Service CRUD
│   ├── auth/
│   │   └── auth_test.go      # Authentication
│   ├── bookings/
│   │   └── bookings_test.go  # User booking flow
│   ├── portfolio/
│   │   └── images_test.go    # Image upload/management
│   ├── scheduling/
│   │   └── scheduling_test.go # Availability logic
│   ├── services/
│   │   └── services_test.go  # Service eligibility
│   ├── user/
│   │   └── profile_test.go   # User profile
│   └── handlers_test.go      # Common handler tests

Running Tests

cd backend

# Run all tests
go test ./...

# Run with verbose output
go test -v ./...

# Run specific test file
go test -v ./handlers/bookings

# Run tests matching pattern
go test -v -run "TestBooking" ./...

Test Database Setup

Tests use a dedicated PostgreSQL database. Set the connection string via:

export TEST_DB_DSN="postgres://user:pass@localhost:5432/crussell_test?sslmode=disable"
go test ./...

Default DSN: postgres://myuser:mypassword@localhost:5432/crussell_test?sslmode=disable

Test Utilities Usage

import (
    "crussell/testutils"
    "crussell/testutils/testdb"
    "crussell/testutils/fixtures"
    "crussell/testutils/jwt"
)

func TestMyHandler(t *testing.T) {
    // Setup test database
    cleanup := testutils.SetupTestDB(t)
    defer cleanup()
    
    // Create test data
    adminID, _ := fixtures.CreateTestAdminUser(db.DB)
    userID, _ := fixtures.CreateTestUser(db.DB)
    serviceID, _ := fixtures.CreateTestService(db.DB)
    
    // Generate tokens
    adminToken := jwt.GenerateAdminToken()
    userToken := jwt.GenerateUserToken(userID)
    
    // Make authenticated requests
    w := testutils.MakeAdminRequest(router, "GET", "/api/admin/bookings", nil, adminID)
    w := testutils.MakeUserRequest(router, "GET", "/api/bookings", nil, userID)
    
    // Assert results
    testutils.AssertStatusCode(t, w, http.StatusOK)
    testutils.AssertJSONResponse(t, w, &response)
}

Test Conventions

  • All test files use //go:build test build tag
  • Database is migrated fresh per test run via testdb.Migrate()
  • Tables are truncated between tests via testdb.TruncateTables()
  • Test tokens use a fixed secret: test-secret-key-for-testing-only
  • Fixtures auto-generate unique emails to avoid conflicts

📂 Environment Variables

Variable Purpose Example
POSTGRES_USER DB username myuser
POSTGRES_PASSWORD DB password mysecret
POSTGRES_DB DB name mydb
JWT_SECRET_KEY HMAC key for JWT (HS256) supersecret
SABRE_DAV_* Optional SabreDAV overrides

Create a .env file in the project root based on the provided .env.example.

Note

: JWT tokens expire after 30 days. Login is rate-limited to 1 attempt per 5 seconds.

📊 Seeding Data

The local-dev-2.sh script automatically seeds:

  • Admin user (admin@example.com / password)
  • Regular user (user@example.com / password)
  • 8 services:
    • 6 standard (no patch test)
    • 2 with patch test requirement (48h) - Gel Polish Full Set, Luxury Gel Manicure

The enhanced local-dev-2.sh script provides additional test data including:

  • Multiple users with different roles and account types
  • Working hours configurations
  • Exceptional working hours groups (holiday schedules)
  • Sample bookings with various statuses
  • Payment records
  • Admin notifications
  • User referrals

If you want to seed manually, use the provided init-scripts/init-script.sql and your favourite Postgres client.

📌 Useful Commands

# Show Docker containers
docker ps

# Rebuild the Go binary and restart containers
make build-backend
docker compose up -d backend

# Tail logs
docker compose logs -f

# Open a shell inside the backend container
docker compose exec backend sh

🏗️ Implementation Status

Complete

Feature Backend Frontend Notes
JWT Authentication Login, register, refresh, middleware; HS256, 30-day expiry, auto-refresh
Booking CRUD ⚠️ Backend complete; frontend submit is stub (logs only)
Admin Endpoints Services, users, today view, notifications
Scheduling System Default hours, exceptional hours, working/available hours
Holiday Hours Integrated in all 3 booking flows
Loyalty Backend DB schema ready, no frontend component
VAT System get_vat_return_data(), calculate_vat() functions exist
User Referrals user_referrals table, backend logic exists
Token Refresh POST /api/refresh-token, auto-refresh in auth store
Portfolio System S3/R2 storage abstraction, tag-based filtering, category filters, admin upload, ?img= featured image param
Service Eligibility Age + patch test filtering; /api/services/eligible-for/{user_id} for admin booking flows
Image Metadata Stripping EXIF/GPS stripped on upload via imaging library
Profile Pictures Upload to separate bucket, cropper, circular display, CalDAV sync
Auto-Booking Status Auto-transition: confirmed → in_progress → completed based on time
Simplified Deposits ⚠️ deposits_required INT on users table (3 default), 48h notice, reduces on payment
Contact Page Dynamic data from first admin user via /api/contact endpoint
Email Verification Verification codes, generate/check endpoints
Calendar Export ⚠️ ICS download endpoint, Add to Calendar button (backend complete)
Loyalty Stamps Backend complete, displayed in account page

⚠️ Partially Complete

Feature Status Details
Notifications Backend only Admin notifications wired; acknowledgment on confirm/cancel; no frontend panel, no push (email/SMS), no regular user notifications
User Notification Preferences DB ready Table user_notification_preferences exists; waiting on user notification system
GDPR Compliance DELETE only anonymize_user(), export_all_user_data() exist but only DELETE is wired to endpoint
Analytics Handler exists handlers/admin/analytics.go exists but NOT wired in router

Not Wired (Handlers Exist)

Handler File Notes
Social Auth handlers/auth/social.go OAuth integration placeholder
Analytics handlers/admin/analytics.go Statistics/dashboard endpoint

🚧 Critical TODOs

Location Issue Priority
BookingFlow.svelte:600 submitBooking() only logs, needs POST implementation High
/api/users/guest Guest endpoint for walk-ins not implemented Medium
GDPR Export Need endpoint for export_all_user_data() Medium
Tax Export Endpoint for VAT return data export Medium

📋 Feature Requests

Feature Description
One-off Custom Services Allow creating single-use services not in regular catalog
One-off Exceptional Hours Single-day overrides without creating a group

🔄 Booking Flows

Crussell supports three distinct booking flows:

Flow User Entry Point Status
Self-Service Customer BookingFlow.svelte/api/bookings ⚠️ Stub submit
Walk-In Admin Admin panel → walk-in modal
Call/Message-In Admin Admin panel → booking modal

All three flows integrate with the holiday/exceptional hours system to prevent bookings during closed periods.


🔍 Helpful Greps

All Three Booking Flows

Backend - All Booking Creation Endpoints:

grep -rn "func.*Create.*Booking\|func.*WalkIn\|func.*Walk.*In\|POST.*booking" backend/handlers/ --include="*.go"

Frontend - All Booking Flow Components:

grep -rln "BookingFlow\|WalkIn\|walk-in\|call.*in\|message.*in" frontend/src/lib/components/ --include="*.svelte"

Combined - Single View of All Booking Flows:

grep -rn "CreateBooking\|CreateWalkIn\|BookingFlow\|submitBooking" backend/ frontend/ --include="*.go" --include="*.svelte"

Database Schema

All Enums:

grep -n "CREATE TYPE" init-scripts/init-script.sql

All Tables:

grep -n "CREATE TABLE" init-scripts/init-script.sql

All Functions:

grep -n "CREATE OR REPLACE FUNCTION" init-scripts/init-script.sql

Router Endpoints

All Wired Routes:

grep -n "r\.\(Get\|Post\|Put\|Delete\|Patch\)" backend/main.go

Middleware Chain:

  • RequestID, RealIP, Logger, Recoverer, Timeout(15s)
  • Security headers (X-Content-Type-Options, X-Frame-Options, X-XSS-Protection)
  • Rate limiting (per-endpoint):
    • Public read-only: 120/min
    • Registration: 10/min
    • Portfolio filters: 60/min
    • Portfolio single image: 120/min
    • Portfolio admin: 60/min
    • Authenticated users: 120/min
    • Admin search: 60/min
    • Admin-only routes: none (trusted)
  • ⚠️ Gap: Rate limiter doesn't read CF-Connecting-IP - behind Cloudflare all users share one bucket
  • ⚠️ Gap: No HSTS header - add when HTTPS working
  • ⚠️ Gap: No Referrer-Policy - for analytics tracking
  • Image metadata stripping - EXIF/GPS stripped on upload (security improvement)

Input Validation:

  • Backend validates all inputs against DB schema constraints
  • Frontend adds maxlength attributes matching DB limits
  • Registration: name (1-50), email (255), phone (20), password (72)
  • Services: name (100), price (>0), duration (1-480), patch test (0-168), age (0-100)
  • Portfolio: tags/filters (256 char max)
grep -n "r\.Use\|r\.Group" backend/main.go

Authentication

JWT Middleware:

grep -rn "JWTMiddleware\|VerifyToken\|ParseToken" backend/ --include="*.go"

Protected Routes:

grep -n "r\.Group.*Auth" backend/main.go

Holiday/Exceptional Hours

Usage Across All Flows:

grep -rn "exceptional.*hours\|holiday.*hours\|getExceptional\|getHoliday" backend/handlers/ frontend/src/ --include="*.go" --include="*.svelte"

Notifications

Admin Notifications:

grep -rn "admin_notifications\|AdminNotification" backend/ --include="*.go"

Notification Reasons (Enum Values):

grep -A10 "admin_notification_reason" init-scripts/init-script.sql

GDPR Functions

Data Export/Anonymization:

grep -rn "anonymize_user\|export_all_user_data\|delete_guest_user" backend/ --include="*.go" init-scripts/

Frontend Debug Artifacts

Console Logs to Remove:

grep -rn "console\.log" frontend/src/ --include="*.svelte" --include="*.ts"

🗄️ Database Schema Overview

Enums

Enum Values
account_role user, admin
account_type standard, vip, guest
booking_status pending, confirmed, in_progress, completed, client_cancelled, we_cancelled, re-schedule, no_show, no_deposit
payment_type deposit, full, refund
payment_method cash, card, bank_transfer, square
payment_status pending, completed, failed, refunded
admin_notification_reason pending_booking, cancelled_booking, rescheduled_booking, 1_week_no_pay, 1_month_no_pay, affiliate_claim, late_cancellation, no_deposit, deposit_paid

Key Tables

Table Purpose
users Customer and admin accounts
verification_codes Email verification and password reset codes
services Salon service catalog
bookings Appointment records
booking_services Services per booking (many-to-many)
payments Payment transactions
working_hours Default weekly schedule
exceptional_working_hours_groups Holiday/special schedules
admin_notifications Admin alerts
user_notification_preferences User notification preferences (email/sms/push)
user_referrals Referral tracking

Validation Rules

Field Rules
Names 1-50 chars, unicode letters/spaces/hyphen/apostrophe/dot
Phone UK format → E.164 (+44...)
Age Must be 16+ years
Login Rate Limit 1 attempt per 5 seconds

Happy coding!

S
Description
Nail salon website with booking system and portfolio
Readme
48 MiB
Languages
Go 64.6%
JavaScript 16.5%
Svelte 14.8%
TypeScript 1.9%
PLpgSQL 1.2%
Other 0.9%