munch-ease-frontend/frontend-spec.md

269 lines
17 KiB
Markdown
Raw Normal View History

2025-11-01 02:31:56 +00:00
# Frontend Specification: Household Multi-Tenancy (v2)
## 1. Objective
Transition the frontend from a single-tenant application to a multi-tenant one centered around the concept of "Households". This involves refactoring the existing authentication system, user onboarding, and data presentation to ensure all information (recipes, meals, shopping lists) is scoped to the active household.
This plan is adapted to the existing codebase, focusing on refactoring rather than starting from scratch.
## 2. Core Concepts
- **Household**: A private group of users. All data is isolated and only visible to members of that household.
- **URL-based Tenancy**: The active household is determined by the URL, e.g., `/the-smiths/recipes`. This makes household context explicit and shareable.
- **Household Switching**: Users belonging to multiple households can switch between them.
- **Authentication**: Evolve the existing system from a simple username login to a robust one supporting email/password and Google OAuth, using JWTs.
## 3. User Flows & UI Requirements
### 3.1. Authentication & Onboarding
- **Login Page (`/login`)**:
- **Refactor `src/components/LoginPage.vue`**.
- Replace the current username input with:
- Email & Password login fields.
- A "Sign in with Google" button.
- Add a link to the "Create Account" page (`/create-account`).
- **Create Account Page (`/create-account`)**:
- **Create a new view `src/views/CreateAccount.vue`**.
- UI components for:
- Email, Display Name, and Password input.
- "Sign up with Google" button.
- A link back to the "Login" page.
- **Post-Login Household Selection (`/welcome`)**:
- **Create a new view `src/views/Welcome.vue`**.
- After a new user logs in for the first time, they are directed here.
- The page should present two choices:
1. **Create a new household**: A simple form with "Household Name".
2. **Join an existing household**: This section should clearly state: "To join a household, ask an existing member to send an invitation to your email address."
- **Invitation Flow**:
- A user receives an email with a link like `https://<app-domain>/invitations/accept?token=...`.
- Visiting this link while logged in should add them to the household and redirect them to that household's dashboard.
### 3.2. In-App Experience
- **Household-Scoped URLs**:
- All application routes must be nested under a household slug: `/:householdSlug/recipes`, `/:householdSlug/shopping`, etc.
- The router must be updated to handle this dynamic parameter. The `householdSlug` will be used in all API calls to fetch household-specific data.
- **Household Switcher**:
- **Create a new component `src/components/HouseholdSwitcher.vue`**.
- Place it in a prominent location (e.g., inside `App.vue`'s navigation bar).
- It should list all households the current user is a member of.
- Clicking a household name should navigate the user to the dashboard of that household, e.g., `/<new-household-slug>/dashboard`.
- **Invite Members UI**:
- **Create a new view `src/views/HouseholdSettings.vue`**.
- It should be accessible at `/:householdSlug/settings/members`.
- It should contain a simple form to enter an email address and a button to "Send Invitation".
- It should also list current household members.
## 4. Actionable Implementation Steps
1. **[x] Refactor Authentication State & API**:
2025-11-01 02:31:56 +00:00
- **Modify `src/api/auth.ts`**:
- Implemented JWT-based login/register using new endpoints; Authorization header injected via client provider.
- `loginWithPassword(email, password)` and `createAccount(email, displayName, password)` now return `User` and set token.
- `logout()` clears token and cached user.
- `currentUser()` now performs token-only refresh (no user in response) and then loads user context via `/api/v1/users/me/households`.
2025-11-01 02:31:56 +00:00
- **Modify `src/composables/useAuth.ts`**:
- Updated to use `User`, added `loginWithPassword` + `logout`, and state for `households` + `activeHousehold`.
- Added `fetchHouseholds()` which hits `/api/v1/users/me/households` and stores state.
2025-11-01 02:31:56 +00:00
2025-11-01 02:44:58 +00:00
2. **[~] Update Router for Multi-Tenancy**:
2025-11-01 02:31:56 +00:00
- **Modify `src/router/index.ts`**:
2025-11-01 02:44:58 +00:00
- Added new public routes: `/create-account`, `/welcome`, and `/invitations/accept`.
- Feature flag `VUE_APP_MULTITENANT_ENABLED` controls nesting:
- When enabled: feature routes are nested under `/:householdSlug/...`.
- When disabled: legacy flat routes remain for backward compatibility.
2025-11-01 02:31:56 +00:00
- **Refactor the `beforeEach` guard**:
- Allows public routes.
- When multitenant flag is on, after auth fetches households via `useAuth().fetchHouseholds()`.
- If none: redirect to `/welcome`.
- If at root (`/`): redirect to first household's `/:householdSlug/mealplan`.
- Ensures `activeHousehold` is set when navigating within a household.
2025-11-01 02:44:58 +00:00
- **Nest existing routes**: Implemented behind feature flag.
2025-11-01 02:31:56 +00:00
3. **[~] Implement Onboarding and Invitation Flows**:
2025-11-01 02:31:56 +00:00
- Build the `Welcome.vue` view for creating the first household.
- Build the `CreateAccount.vue` view.
- Build the "Accept Invitation" page (`/invitations/accept?token=...`). It should take the token from the URL, call the API, and redirect on success.
- Status: Welcome page implements create-household flow using `POST /api/v1/households` and redirects to `/:slug/mealplan`. Create Account UI implemented. Invitation Accept implemented: reads `token` from query, calls `POST /api/v1/invitations/accept`, and redirects to the accepted household. Uses a temporary raw fetch helper until OpenAPI adds this endpoint.
2025-11-01 02:31:56 +00:00
2025-11-01 02:44:58 +00:00
4. **[~] Integrate Household Context into the App**:
- **Create `src/composables/useHousehold.ts`**: Implemented. Extracts `householdSlug` from route and binds provider to API client.
- **Implement `HouseholdSwitcher.vue`**: Implemented minimal version and mounted in `App.vue`.
- **Update API Services**: Implemented header-injection in `src/api/client.ts` via `X-Household-Slug` and `Authorization` using configurable providers; no path changes.
- Verified by `tests/household.header.test.ts` and auth tests.
2025-11-01 02:31:56 +00:00
5. **[~] Implement Invitation UI**:
- Build the `HouseholdSettings.vue` view for inviting members and listing current members.
- Status: Invite form implemented (sends email via POST `/api/v1/invitations`). Members listing pending backend endpoint.
2025-11-01 02:31:56 +00:00
6. **[ ] Final Review & Cleanup**:
- Remove the old `persons` concept from the frontend code. The `user` from `useAuth` is now the primary identity.
- Ensure all data displays are correctly filtered by the active household by verifying the `householdSlug` is passed in all API calls.
- Test all user flows: new user signup, login, creating a household, joining via invitation, and switching between households.
---
## 0. Current State (Nov 2025) and Gap Analysis
Current implementation is single-tenant with username-based login and no household context.
What exists today
- Auth
- `src/api/auth.ts`: username login (`login(username: string)`), `currentUser()` via cookie `user_id`.
- `src/composables/useAuth.ts`: stores `Person | null`, exposes `login(username)` and `loadUser()`.
- `src/components/LoginPage.vue`: lists persons and logs in by selected person name.
- Routing
- `src/router/index.ts`: flat routes (`/recipes`, `/shopping`, `/mealplan`); no `/:householdSlug` nesting or redirects.
- Guard checks `requiresAuth` only; no household awareness.
- SDK/API
- `src/api/sdk.ts`: calls `/api/v1/...` without household context.
- Generated OpenAPI types do not include household slug in paths; shapes are single-tenant.
- Domain & UI
- Identity revolves around `Person`; no household model, switcher, or settings views.
Gaps vs requirements
- Authentication & onboarding
- Implemented: JWT `loginWithPassword`, `logout`, account creation via `createAccount` with token provider wiring.
- UI: `LoginPage.vue` shows email/password form under feature flag; `CreateAccount.vue` view implemented; `Welcome.vue` creates household; Invitation Accept implemented; Google OAuth pending.
2025-11-01 02:31:56 +00:00
- Routing & URL-based tenancy
2025-11-01 02:44:58 +00:00
- Partial: Feature-flagged nesting implemented; still need guard logic for fetching households and redirects.
- `useHousehold.ts` and `HouseholdSwitcher.vue` added; further wiring to fetch households pending.
2025-11-01 02:31:56 +00:00
- API boundary
- No household scoping passed to backend. Need a typed strategy (prefer header parameter) without breaking OpenAPI typing.
- Cleanup
- `persons` used across login and tests; must be deprecated in favor of authenticated `user` and their households.
Assumptions and constraints
- Preserve strict TS (no `any`/`unknown` in app code) and keep OpenAPI as source of truth.
- Do not rewrite typed API paths in code; prefer header or OpenAPI param for household scoping.
---
2025-11-01 05:21:23 +00:00
## Amendment: API Services and Typing Strategy (Updated Nov 1, 2025)
The backend OpenAPI has been updated and now exposes `householdSlug` as a path parameter for scoped endpoints (recipes, meals, shopping, invitations, whoami). Auth endpoints are fully typed (login/register/refresh/logout). Invitation acceptance remains a global endpoint (`/api/v1/invitations/accept`) with a typed operation.
Implications and actions:
- Replace the temporary `X-Household-Slug` header injection with typed path parameters.
- Migrate all API calls to pass `{ params: { path: { householdSlug } } }` where required.
- Remove `setHouseholdSlugProvider` usage once the migration is complete.
- Auth refresh now returns only `{ accessToken, tokenType }` (no `user` object). The `currentUser()` flow must:
1) Call refresh to obtain a token and set the Authorization provider.
2) Load user/household context via typed endpoints (e.g., `GET /api/v1/users/me/households`).
3) Optional: Use `GET /api/v1/households/{householdSlug}/whoami` to validate membership for the active route.
- Invitations:
- Create invitation is now typed at `POST /api/v1/households/{householdSlug}/invitations`.
- Accept invitation is typed at `POST /api/v1/invitations/accept`.
- Remove temporary raw fetch usage for invitations and switch to the generated client.
- Household members listing endpoint is still not present in OpenAPI; continue using the temporary fetch wrapper until the backend exposes it.
2025-11-01 02:44:58 +00:00
---
Progress Log (Nov 1, 2025)
- Established green baseline (typecheck + tests pass).
- Added auth API tests driving a minimal multitenant-ready surface.
- Implemented `loginWithPassword`, `logout`, and stubs in `src/api/auth.ts` to satisfy tests.
- Refactored `useAuth` to add households and activeHousehold state, plus `loginWithPassword` and `logout`. Tests added and passing.
- Added router tests and implemented feature-flagged nested routes and new public routes. Placeholders for onboarding/invitations added.
- Implemented `useHousehold.ts`, header + auth providers in API client, minimal `HouseholdSwitcher.vue`, and mounted it. Added a header injection test.
- Implemented JWT login/register in `auth.ts` and wired token to client provider. `useAuth` updated with households fetching. `currentUser` refactored to token-only refresh plus households load.
- Router guard updated to handle public/multitenant routing and redirects.
- Added `useAuth.createAccount` with state update and tests for it; implemented `CreateAccount.vue` with form and navigation.
- Implemented Invitation Accept flow: added `src/api/invitations.ts` with `acceptInvitation` (temporary raw fetch), `InvitationAccept.vue` reads token and redirects to household; added `tests/invitations.api.test.ts`.
- Next: Implement Household Settings (invite members form), then remove legacy Person UI.
- Added a Settings link to `HouseholdSwitcher.vue` to surface the `household-settings` route for easier discovery.
- Implemented Household Settings invite form and members list UI. `src/views/HouseholdSettings.vue` now loads members via a temporary `listMembers()` in `src/api/households.ts` using the raw fetch helper. When the backend exposes a typed endpoint, we will swap to the generated client.
- Fixed router tests by using memory history in non-browser environments to avoid relying on `window.location` during unit tests.
2025-11-01 05:21:23 +00:00
- Backend updated OpenAPI and codegen has been run:
- Many endpoints are now path-scoped with `{householdSlug}` (recipes, meals, shopping, invitations (create), whoami).
- Auth endpoints (login/register/refresh/logout) are fully typed; `refresh` returns only `{ accessToken, tokenType }`.
- Action: begin migrating API usage to typed path parameters and remove temporary header injection and fetch helpers.
2025-11-01 02:31:56 +00:00
---
## Detailed Tasks by File/Module
New files
- `src/views/CreateAccount.vue`: Email, Display Name, Password; Google signup; link to login.
- `src/views/Welcome.vue`: Create first household or instructions to join via invite.
- `src/views/HouseholdSettings.vue`: Invite members by email; list members.
- `src/components/HouseholdSwitcher.vue`: Lists user households and navigates to selected household dashboard.
- `src/composables/useHousehold.ts`: Exposes `activeHouseholdSlug` from route and small helpers.
Auth
- `src/api/auth.ts`:
- Replace `login(username: string)``login(email: string, password: string)`.
- Add `createAccount`, `handleGoogleLogin`, `logout`; update `currentUser` to refresh JWT/session.
- Store token per backend guidance; clear on `logout`.
- `src/composables/useAuth.ts`:
- Manage `user`, `households`, `activeHousehold` state; expose `login`, `register`, `handleGoogleLogin`, `logout`, `loadUser`, `setActiveHousehold`.
Router
- `src/router/index.ts`:
- Add public routes: `/create-account`, `/welcome`, `/invitations/accept`.
- Nest feature routes under `/:householdSlug`.
- Guard: after auth, fetch households; redirect root `/` to first household dashboard; route `/welcome` if none.
- Update imperative navigations to include `{ householdSlug }` via named routes.
API client
- `src/api/client.ts`:
2025-11-01 05:21:23 +00:00
- Previous: header injection (`X-Household-Slug`) from a configurable getter to scope requests.
- Now: migrate calls to use typed `{ params: { path: { householdSlug } } }` and remove the `X-Household-Slug` header provider.
- Authorization provider remains as-is, fed by the JWT token from login/refresh.
2025-11-01 02:31:56 +00:00
SDK
- `src/api/sdk.ts`:
- No path changes; ensure all calls work with new auth and household header.
UI
- Login Page: Refactored to show email/password form when multitenant flag is enabled; legacy person list retained otherwise. Link to Create Account added.
2025-11-01 02:31:56 +00:00
- Add `HouseholdSwitcher.vue` to app chrome and wire with router.
- Update components that navigate using string paths to use named routes with slug.
- `src/views/HouseholdSettings.vue`: Invite members form wired to `sendInvitation(email)`. Members list rendered from `listMembers()`; guarded for backends that dont yet support the endpoint.
2025-11-01 05:21:23 +00:00
- Adjust pages that call SDK/API to pass `{ householdSlug }` path params once client services are migrated.
2025-11-01 02:31:56 +00:00
Tests
- Update MSW handlers/tests to assume JWT auth and household header.
- Add tests for router guards, invitation acceptance, and household switching.
- Add API tests for invitations (accept/send) and members listing header behavior. Router tests run under memory history in tests.
2025-11-01 05:21:23 +00:00
- Update tests to assert that calls pass `householdSlug` via typed params instead of relying on an injected header.
2025-11-01 02:31:56 +00:00
---
2025-11-01 05:21:23 +00:00
## OpenAPI & Typing Considerations (Updated)
2025-11-01 02:31:56 +00:00
- Avoid `any`/`unknown` in app code; keep all API calls typed via `openapi-fetch`.
2025-11-01 05:21:23 +00:00
- Household scoping: use typed path params (`{ params: { path: { householdSlug } } }`); do not mutate path strings.
- Auth refresh: returns `{ accessToken, tokenType }`. After refresh, call user/household endpoints to populate app state. Use `whoami` to validate the active routes slug when needed.
- Cleanup/migration tasks:
1) Remove X-Household-Slug header injection in `api/client.ts` and refactor services to accept `householdSlug` via typed params.
2) Replace temporary raw fetch for invitations (both accept and create) and members listing with generated typed endpoints. Invitations are now typed; members listing remains pending.
3) Legacy identity: continue using `User` as the primary identity. Keep `Person` in meal-related UIs where the backend requires it, but remove Person as the login/identity concept.
2025-11-01 02:31:56 +00:00
---
## Migration Plan & Feature Flag
- Optional `MULTITENANT_ENABLED` flag for staged rollout of routes and UI.
- Keep legacy login until backend endpoints are ready; hide persons UI once households exist for a user.
---
## Acceptance Criteria (Summary)
- Users can create accounts, login (email/password, Google), logout, and refresh sessions.
- All routes operate under `/:householdSlug` with correct redirects and deep link support.
- Active household is selectable and visible; API calls are correctly scoped.
- Invitation token acceptance adds membership and navigates to the household dashboard.
- Legacy persons login removed from UI; tests updated and passing.
---
## Open Questions
- Backend: path vs header vs cookie for `householdSlug`? Confirm to finalize client strategy.
- Exact OpenAPI shapes for `User`, `Household`, `Invitation` endpoints.
- Google OAuth flow pattern (token exchange vs redirect).
- JWT storage medium per security guidance (cookie vs localStorage).