# 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. 1. Member visits `/members/claim` and enters their email. 2. Backend checks the email exists AND is unclaimed — sends a 6-character code. 3. Member visits `/members/claim/verify`, enters the code and chooses a password. 4. 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/request` to prevent email enumeration. ### Onboarding Lifecycle Member accounts move through these states: 1. `invited` 2. `onboarding` 3. `pending_contract` 4. `pending_review` 5. `active` Only `active` members can use the normal members area routes like dashboard, bookings, walks, profile, and messages. ### Login (returning and onboarding members) 1. Member visits `/members/login`, enters email + password. 2. If credentials are valid, a **2FA code** is emailed. 3. Member visits `/members/login/verify`, enters the code. 4. On success, a JWT access token (15 min) and refresh token (7 days) are issued and stored in `localStorage`. 5. If the member is not yet `active`, the frontend routes them into `/members/onboarding` instead 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`. | --- ## Email In development, set `EMAIL_BACKEND=console` (default) in `.env` — codes are printed to the backend stdout. No SMTP server required. For production, configure: ```env 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: ```bash cd backend alembic upgrade head ``` Migration files (in order): - `alembic/versions/a1b2c3d4e5f6_add_members.py` - `alembic/versions/c7d2b6f4a9e1_add_member_onboarding_lifecycle.py` - `alembic/versions/a7f3e2c1b8d4_add_message_soft_delete_and_reply.py` --- ## Creating an Onboarding Client (admin workflow) Use the admin JWT to create an onboarding account: ```bash curl -X POST http://localhost:8000/api/v1/admin/members \ -H "Authorization: Bearer " \ -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: 1. goes to `/members/claim` 2. claims the account and sets a password 3. logs in 4. completes `/members/onboarding` 5. signs the contract 6. waits for admin activation Admin then activates the member with: ```bash curl -X POST http://localhost:8000/api/v1/admin/members//activate \ -H "Authorization: Bearer " ``` --- ## Recording a Walk (admin workflow) ```bash curl -X POST http://localhost:8000/api/v1/admin/walks \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "member_id": "", "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/onboarding` after login and routes non-active users to `/members/onboarding`. - **Token storage**: `localStorage` keys `member_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_at` timestamp); 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 via `reply_to_id` and appear in a thread below the original message body. --- ## Security Notes - Member tokens carry `role: "member"` in the JWT payload. - `get_authenticated_member` allows signed-in onboarding users to access onboarding-only routes. - `get_current_member` only allows members with `member_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_at` and `used_at` columns — 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_id` ownership before acting — members cannot affect each other's messages.