munch-ease-frontend/frontend-spec.md
2025-11-01 17:31:03 +11:00

16 KiB
Raw Blame History

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:
    • 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.
    • 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.
  1. [~] Update Router for Multi-Tenancy:

    • Modify src/router/index.ts:
      • 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.
      • 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.
      • Nest existing routes: Implemented behind feature flag.
  2. [~] Implement Onboarding and Invitation Flows:

    • 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.
  1. [~] 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.
  2. [~] 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.
  1. [ ] 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 1, 2025)

All core features are migrated to multi-tenancy with path-scoped endpoints and token-based auth. The codebase no longer uses the X-Household-Slug header. Tests and type checks are fully green.

What exists now

  • Auth
    • src/api/auth.ts: email/password login and register; token-only refresh in currentUser() which then loads /api/v1/users/me/households.
    • src/composables/useAuth.ts: manages user, households, and activeHousehold; exposes login/logout/createAccount and fetchHouseholds().
  • Routing
    • src/router/index.ts: feature-flagged nesting under /:householdSlug/...; public routes include /create-account, /welcome, and /invitations/accept.
    • Guard fetches households, redirects root / to the first household's mealplan, and uses memory history in tests (hash in browser).
  • SDK/API
    • src/api/sdk.ts: recipes, meals, and shopping are migrated to /api/v1/households/{householdSlug}/... typed endpoints. Persons and parse use temporary raw fetch endpoints where OpenAPI lacks coverage.
    • src/api/client.ts: Authorization header provider only; household header injection removed.
  • Domain & UI
    • Member arrays (chefs, consumers, cleanup) normalized to MemberRef { id, displayName } with decoders handling legacy shapes gracefully.
    • Invitation Accept flow implemented; Household Settings supports sending invitations and listing members (members via temporary raw fetch path-scoped endpoint until typed spec lands).

Status of tests and typing

  • All tests pass: 27 files, 48 tests.
  • tsc and vue-tsc pass with no errors.

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 (completed):

  • Removed X-Household-Slug and migrated to typed path parameters across recipes, meals, and shopping.
  • currentUser() updated to token-only refresh and household loading.
  • Invitations: sending is typed under household scope; accept is typed globally.
  • Members listing is temporarily fetched via a path-scoped raw endpoint until OpenAPI includes it.

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, Authorization provider in API client, minimal HouseholdSwitcher.vue, and mounted it. Replaced header injection test with path-scoped assertion.
  • 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.
  • Router uses memory history in tests to avoid relying on window.location.
  • 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 }.
    • Completed migration to typed path parameters; header injection removed; only small raw fetch helpers remain for endpoints not yet in OpenAPI (persons, parse, members list).

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:
    • Uses typed { params: { path: { householdSlug } } } across SDK; X-Household-Slug header provider removed.
    • Authorization provider remains as-is, fed by JWT token from login/refresh.

SDK

  • src/api/sdk.ts:
    • Recipes, meals, shopping fully path-scoped to households. Persons/parse remain via raw fetch until typed coverage.

UI

  • Login Page: Refactored to show email/password form when multitenant flag is enabled; legacy person list retained otherwise. Link to Create Account added.
  • 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.
  • Adjust pages that call SDK/API to pass { householdSlug } path params once client services are migrated.

Tests

  • MSW handlers updated for path-scoped endpoints; Authorization header assertions retained where relevant.
  • Router tests run under memory history.
  • Replaced header injection test with path-scoped recipe list test.

OpenAPI & Typing Considerations (Updated)

  • Avoid any/unknown in app code; keep all API calls typed via openapi-fetch.
  • 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.

Migration Plan & Feature Flag

  • VUE_APP_MULTITENANT_ENABLED flag controls nested household routes. With the migration complete, keep this flag for rollout control; default can be enabled once backend is stable across environments.

Acceptance Criteria (Summary)

  • Users can create accounts, login (email/password), logout, and refresh sessions (token-only).
  • All routes operate under /:householdSlug with correct redirects and deep link support.
  • Active household is selectable and visible; API calls are correctly scoped via path params.
  • Invitation token acceptance adds membership and navigates appropriately.
  • Legacy persons identity is no longer used in auth; MemberRef used in meal UIs; tests updated and passing.

Google OAuth is planned next.


Open Questions / Next Steps

  • Add a user profile endpoint and load it post-refresh to populate currentUser() with real data instead of a placeholder.
  • Replace temporary raw fetch calls (persons, parse, members listing) with typed endpoints when available.
  • Implement Google OAuth login and account creation flows.
  • Review MyShoppingPage usage and remove legacy getMyShoppingList/saveMyShoppingList stubs when UI is refactored or removed.