This commit is contained in:
ponzischeme89
2026-04-18 07:23:55 +12:00
parent f210020772
commit 6d44e05de4
396 changed files with 75296 additions and 0 deletions
+261
View File
@@ -0,0 +1,261 @@
# 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 <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:
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/<member-uuid>/activate \
-H "Authorization: Bearer <admin_token>"
```
---
## Recording a Walk (admin workflow)
```bash
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/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.