v1
This commit is contained in:
+261
@@ -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.
|
||||
Reference in New Issue
Block a user