9.8 KiB
Members Area
A password-protected portal for Goodwalk clients. It supports an onboarding lifecycle before full activation: admin creates the client, the client claims the account, completes onboarding details, signs the contract, and only then is the account activated for the normal members area.
Overview
| Area | URL |
|---|---|
| Login | /members/login |
| Login 2FA verify | /members/login/verify |
| Claim account | /members/claim |
| Claim verify + set password | /members/claim/verify |
| Onboarding | /members/onboarding |
| Dashboard | /members/dashboard |
| Book a walk | /members/book |
| Walk history | /members/walks |
| Profile | /members/profile |
| Contract / onboarding | /members/contract |
| Messages | /members/messages |
Authentication Flow
Account Claim (first-time setup)
Members are pre-registered by the admin via POST /api/v1/admin/members. They do not have a password yet.
- Member visits
/members/claimand enters their email. - Backend checks the email exists AND is unclaimed — sends a 6-character code.
- Member visits
/members/claim/verify, enters the code and chooses a password. - Account is claimed (
is_claimed = true, password set) and moves into the onboarding lifecycle.
The claim code expires in 15 minutes. A generic response is always returned from
/members/claim/requestto prevent email enumeration.
Onboarding Lifecycle
Member accounts move through these states:
invitedonboardingpending_contractpending_reviewactive
Only active members can use the normal members area routes like dashboard, bookings, walks, profile, and messages.
Login (returning and onboarding members)
- Member visits
/members/login, enters email + password. - If credentials are valid, a 2FA code is emailed.
- Member visits
/members/login/verify, enters the code. - On success, a JWT access token (15 min) and refresh token (7 days) are issued and stored in
localStorage. - If the member is not yet
active, the frontend routes them into/members/onboardinginstead of the dashboard.
The 2FA code expires in 10 minutes. Both codes are SHA-256 hashed before storage.
Token Refresh
The frontend memberApi.js transparently retries failed 401 responses with a token refresh via POST /api/v1/members/auth/refresh. If refresh fails, tokens are cleared and the user is redirected to login.
Backend API
All endpoints are under /api/v1/ prefix.
Public (rate-limited)
| Method | Path | Description |
|---|---|---|
POST |
/members/claim/request |
Send claim code (5/min) |
POST |
/members/claim/complete |
Verify code + set password (10/min) |
POST |
/members/auth/login |
Password check → send 2FA (10/min) |
POST |
/members/auth/login/verify |
Verify 2FA → issue JWT pair (10/min) |
POST |
/members/auth/refresh |
Rotate refresh token (10/min) |
Member-authenticated (Bearer JWT with role: "member")
| Method | Path | Description |
|---|---|---|
GET |
/members/me |
Own profile |
PUT |
/members/me |
Update contact details |
GET |
/members/onboarding |
Read onboarding lifecycle + onboarding data |
PUT |
/members/onboarding |
Save onboarding details and mark onboarding complete |
POST |
/members/onboarding/contract |
Sign the service agreement |
GET |
/members/walks |
Completed walks (newest first) |
GET |
/members/bookings |
All bookings |
POST |
/members/bookings |
Request a new booking |
GET |
/members/contract |
Onboarding data + contract info |
GET |
/members/messages |
Admin messages — excludes soft-deleted (newest first) |
PUT |
/members/messages/{id}/read |
Mark a message as read |
DELETE |
/members/messages/{id} |
Soft-delete (dismiss) a message |
POST |
/members/messages/{id}/reply |
Send a reply to an admin message |
Admin-authenticated (Bearer JWT with admin role)
| Method | Path | Description |
|---|---|---|
POST |
/admin/members |
Pre-register a member |
GET |
/admin/members |
List all members |
POST |
/admin/members/{member_id}/activate |
Activate a fully completed onboarding account |
POST |
/admin/walks |
Record a completed walk |
POST |
/admin/messages |
Send a message to a member |
GET |
/admin/messages |
List all admin-sent messages with per-member read status |
Database Tables
| Table | Purpose |
|---|---|
members |
Member profiles plus lifecycle fields such as member_status, claimed_at, onboarding_completed_at, contract_signed_at, and activated_at |
member_verification_codes |
Claim and 2FA codes (hashed, with expiry and used_at) |
member_refresh_tokens |
Member JWT refresh tokens (hashed, revoked on rotation) |
walks |
Completed walks recorded by admin |
bookings |
Walk booking requests from members |
admin_messages |
Messages sent from admin to members. Also stores member replies (direction = "outbound", reply_to_id links back to original). Soft-deleted via deleted_at. |
In development, set EMAIL_BACKEND=console (default) in .env — codes are printed to the backend stdout. No SMTP server required.
For production, configure:
EMAIL_BACKEND=smtp
SMTP_HOST=smtp.yourprovider.com
SMTP_PORT=587
SMTP_USE_TLS=true
SMTP_USER=your@email.com
SMTP_PASSWORD=yourpassword
EMAIL_FROM=noreply@goodwalk.co.nz
Database Migration
Run the Alembic migrations to create the member tables and onboarding lifecycle columns:
cd backend
alembic upgrade head
Migration files (in order):
alembic/versions/a1b2c3d4e5f6_add_members.pyalembic/versions/c7d2b6f4a9e1_add_member_onboarding_lifecycle.pyalembic/versions/a7f3e2c1b8d4_add_message_soft_delete_and_reply.py
Creating an Onboarding Client (admin workflow)
Use the admin JWT to create an onboarding account:
curl -X POST http://localhost:8000/api/v1/admin/members \
-H "Authorization: Bearer <admin_token>" \
-H "Content-Type: application/json" \
-d '{
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Smith",
"phone": "021 234 5678",
"address": "123 Main St, Auckland",
"emergency_contact": "John Smith 021 987 6543",
"onboarding_data": {
"dog_name": "Buddy",
"dog_breed": "Labrador",
"vet_name": "Auckland Vets",
"vet_phone": "09 123 4567",
"vaccinations_up_to_date": true,
"service": "Pack Walk"
}
}'
The client then:
- goes to
/members/claim - claims the account and sets a password
- logs in
- completes
/members/onboarding - signs the contract
- waits for admin activation
Admin then activates the member with:
curl -X POST http://localhost:8000/api/v1/admin/members/<member-uuid>/activate \
-H "Authorization: Bearer <admin_token>"
Recording a Walk (admin workflow)
curl -X POST http://localhost:8000/api/v1/admin/walks \
-H "Authorization: Bearer <admin_token>" \
-H "Content-Type: application/json" \
-d '{
"member_id": "<member-uuid>",
"walked_at": "2026-03-31T09:00:00+13:00",
"service_type": "pack_walk",
"duration_minutes": 60,
"notes": "Buddy was a superstar today!"
}'
Frontend Architecture
- Layout:
frontend/src/routes/members/+layout.svelte— sticky nav bar, auth guard, lifecycle-aware routing, logout button. - Lifecycle guard: the layout reads
/api/v1/members/onboardingafter login and routes non-active users to/members/onboarding. - Token storage:
localStoragekeysmember_access_token/member_refresh_token. - API client:
frontend/src/lib/memberApi.js— wraps all API calls, handles 401 retry with token refresh. - Root layout: Updated to skip the main site Header/Footer for
/members/*routes.
Page design system
All members area pages share a consistent visual language (dark green #213021, yellow #FFD100, warm neutral surfaces). Key patterns:
| Pattern | Usage |
|---|---|
| Dark hero header | Dashboard, Walks — full-bleed dark green gradient with yellow CTA |
| Light card header | Book, Profile, Messages — white/cream card with yellow radial accent |
| Step accordion | Book a walk — collapsible numbered steps with animated open/close |
| Split workspace | Messages — inbox list (300 px) + reader panel, collapses to single-column on mobile |
| Skeleton loaders | All pages — shape-matched placeholder surfaces while data loads |
Messages — Member Features
Members can manage their inbox directly from /members/messages:
- Read tracking — messages are automatically marked as read when opened.
- Delete — members can dismiss messages. A confirmation step is shown before deletion. Deletion is a soft-delete on the backend (
deleted_attimestamp); the message is hidden from the member but retained for admin records. - Reply — members can reply to any message from within the reader panel. Replies are stored as
direction = "outbound"records linked viareply_to_idand appear in a thread below the original message body.
Security Notes
- Member tokens carry
role: "member"in the JWT payload. get_authenticated_memberallows signed-in onboarding users to access onboarding-only routes.get_current_memberonly allows members withmember_status = "active"to access the full members area.- Verification codes are SHA-256 hashed before storage (same as refresh tokens). Plaintext is only ever sent via email.
- All verification codes have explicit
expires_atandused_atcolumns — replay attacks are blocked. - Claim requests always return a generic message to prevent email enumeration.
- All auth endpoints are rate-limited via slowapi.
- Message delete and reply endpoints verify
member_idownership before acting — members cannot affect each other's messages.