diff --git a/frontend-spec.md b/frontend-spec.md
index a2bb347..17b34e9 100644
--- a/frontend-spec.md
+++ b/frontend-spec.md
@@ -3,7 +3,7 @@
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.
Status of tests and typing
-- All tests pass: 27 files, 48 tests (router tests fixed via memory history fallback in non-browser envs).
+- All tests pass: 29 files, 49 tests (slug-only routes; memory history fallback in non-browser envs; unauthenticated and refresh-401 guard redirects covered).
- `tsc` and `vue-tsc` pass with no errors.
# Frontend Specification: Household Multi-Tenancy (v2)
@@ -78,33 +78,31 @@ This plan is adapted to the existing codebase, focusing on refactoring rather th
2. **[x] 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.
+ - Removed legacy flag and flat routes: feature routes are always nested under `/:householdSlug/...`.
- **Refactor the `beforeEach` guard**:
- Allows public routes.
- - When multitenant flag is on, after auth fetches households via `useAuth().fetchHouseholds()`.
+ - 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.
- - **History behavior**: Uses hash history in real browsers and memory history in tests/SSR (detected via `globalThis.location`). Router tests verify both flag modes.
+ - **Nest existing routes**: Always under `/:householdSlug` (legacy flat routes removed).
+ - **History behavior**: Uses hash history in real browsers and memory history in tests/SSR (detected via `globalThis.location`). Router tests assert slug-only mode.
3. **[~] 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.
+ - 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 typed `POST /api/v1/invitations/accept`, and redirects to the accepted household.
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.
+ - **Update API Services**: No household header injection; Authorization header provided by token provider. Cookies are only sent for the refresh endpoint; all other requests avoid credentials. SDK/services use typed path params.
+ - Verified by `tests/household.header.test.ts`, auth tests, and router guard tests.
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.
+ - Status: Invite form implemented (sends email via POST `/api/v1/invitations`). Members listing implemented using typed endpoint `GET /api/v1/households/{householdSlug}/members`.
6. **[ ] Final Review & Cleanup**:
- Remove the old `persons` concept from the frontend code. The `user` from `useAuth` is now the primary identity.
@@ -122,7 +120,7 @@ What exists now
- `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`.
+ - `src/router/index.ts`: slug-only 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.
@@ -133,7 +131,7 @@ What exists now
- MyShopping: page remains as in master with editable panel backed by legacy v1 stubs (`getMyShoppingList/saveMyShoppingList`) pending backend ad-hoc item endpoints.
Status of tests and typing
-- All tests pass: 27 files, 48 tests.
+- All tests pass: 29 files, 49 tests.
- `tsc` and `vue-tsc` pass with no errors.
---
@@ -160,11 +158,11 @@ Progress Log (Nov 1, 2025)
- 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`.
+- Implemented Invitation Accept flow: added `src/api/invitations.ts` with typed `acceptInvitation`, `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`.
+ - Implemented Household Settings invite form and members list UI. `src/views/HouseholdSettings.vue` now loads members via the typed endpoint.
+ - Router uses memory history in tests to avoid relying on `window.location`. Added unauthenticated and refresh-401 guard redirect tests.
- 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 }`.
@@ -235,7 +233,7 @@ Tests
- Auth refresh: returns `{ accessToken, tokenType }`. After refresh, call user/household endpoints to populate app state. Use `whoami` to validate the active route’s 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. (Completed)
- 2) Replace temporary raw fetch calls with generated typed endpoints where available: invitations and members listing are now typed; migrate usages. (Members listing migrated) Persons and parse remain raw for now.
+ 2) Replace temporary raw fetch calls with generated typed endpoints where available: invitations and members listing are now typed; migrate usages. Persons and parse remain raw for now.
3) Integrate the new `POST /shopping/current/ingredients` endpoint into the SDK (`requestIngredient(ingredientId: number)`) and expose via `useShopping`; refactor `MyShoppingPage.vue` accordingly and remove legacy stubs. (SDK + composable done; UI refactor next)
4) Legacy identity: continue using `User` as the primary identity. Keep `Person` in meal-related UIs where required by backend, but remove Person as the login/identity concept.
@@ -243,7 +241,7 @@ Tests
## 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.
+- Removed `VUE_APP_MULTITENANT_ENABLED` and legacy flat routes. The app now always uses slug-scoped routes.
---
diff --git a/src/api/client.ts b/src/api/client.ts
index 9c387cb..6f3bc9d 100644
--- a/src/api/client.ts
+++ b/src/api/client.ts
@@ -25,15 +25,22 @@ export function setAuthTokenProvider(provider: (() => string | null) | null) {
authTokenProvider = provider
}
-function isRefreshRequest(input: RequestInfo | URL): boolean {
+function requestInfoToUrl(input: RequestInfo | URL): string | null {
try {
- const url = typeof input === 'string' ? input : (input as URL).toString()
- return url.includes('/api/v1/auth/refresh')
+ if (typeof input === 'string') return input
+ if (typeof URL !== 'undefined' && input instanceof URL) return input.toString()
+ if (typeof Request !== 'undefined' && input instanceof Request) return input.url
+ return String(input)
} catch {
- return false
+ return null
}
}
+function isRefreshRequest(input: RequestInfo | URL): boolean {
+ const url = requestInfoToUrl(input)
+ return !!url && url.includes('/api/v1/auth/refresh')
+}
+
export const api = createClient