diff --git a/frontend-spec.md b/frontend-spec.md index b814085..0bfb580 100644 --- a/frontend-spec.md +++ b/frontend-spec.md @@ -59,13 +59,13 @@ This plan is adapted to the existing codebase, focusing on refactoring rather th 1. **[~] Refactor Authentication State & API**: - **Modify `src/api/auth.ts`**: - - Added minimal multitenant-ready surface while preserving legacy login: - - `loginWithPassword(email, password)` implemented by delegating to legacy `login(username)`; tests added. - - `logout()` clears cached user; covered by tests. - - Stubs for `createAccount(email, displayName, password)` and `handleGoogleLogin(token)` throw pending-implementation errors; tests assert presence. - - Token/JWT storage is still cookie-based (per current backend); no localStorage changes yet. + - 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()` remains a bridge to cookie-based refresh; adapts legacy `Person` to `User` temporarily. - **Modify `src/composables/useAuth.ts`**: - - TODO: Update to accept email/password and expose `register`, `logout`, `households`, and `activeHousehold`. Not started. + - Updated to use `User`, added `loginWithPassword` + `logout`, and state for `households` + `activeHousehold`. + - Added `fetchHouseholds()` which hits `/api/v1/users/me/households` and stores state. 2. **[~] Update Router for Multi-Tenancy**: - **Modify `src/router/index.ts`**: @@ -74,23 +74,24 @@ This plan is adapted to the existing codebase, focusing on refactoring rather th - When enabled: feature routes are nested under `/:householdSlug/...`. - When disabled: legacy flat routes remain for backward compatibility. - **Refactor the `beforeEach` guard**: - - It should allow access to public routes. - - After login, it must fetch the user's households. - - If the user has no households, redirect to `/welcome`. - - If the user has households but is at the root (`/`), redirect to the first household's dashboard (e.g., `/${householdSlug}/dashboard`). + - 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. 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: Placeholder views created for all three; logic pending. + - Status: Placeholder views created for all three; logic pending. Household creation endpoint available. 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` using a configurable provider; no path changes. - - Verified by `tests/household.header.test.ts`. + - **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. 5. **[ ] Implement Invitation UI**: - Build the `HouseholdSettings.vue` view for inviting members and listing current members. @@ -157,8 +158,10 @@ Progress Log (Nov 1, 2025) - 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 injection provider in API client, minimal `HouseholdSwitcher.vue`, and mounted it. Added a header injection test. -- Next: Update `LoginPage.vue` to email/password UI using `useAuth.loginWithPassword`; implement router guard post-login household redirects; scaffold `HouseholdSettings.vue` and households fetching in `useAuth`. +- 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. +- Router guard updated to handle public/multitenant routing and redirects. +- Next: Update `LoginPage.vue` to email/password UI using `useAuth.loginWithPassword`; implement Create Account UI; wire `Welcome.vue` to create household. --- diff --git a/src/composables/useAuth.ts b/src/composables/useAuth.ts index a14a979..fd0f9e7 100644 --- a/src/composables/useAuth.ts +++ b/src/composables/useAuth.ts @@ -1,5 +1,6 @@ import { ref } from 'vue' import { currentUser as apiCurrentUser, login as apiLogin, loginWithPassword as apiLoginWithPassword, logout as apiLogout } from '@/api/auth' +import { api } from '@/api/client' import type { User } from '@/domain/types' type Household = { id: number; name: string; slug: string } @@ -51,5 +52,13 @@ export function setActiveHousehold(h: Household | string | null) { } export function useAuth() { - return { user, households, activeHousehold, loadUser, login, loginWithPassword, logout, setHouseholds, setActiveHousehold } + async function fetchHouseholds(): Promise { + const res = await api.GET('/api/v1/users/me/households', { params: {} }) + const list = Array.isArray(res.data) ? res.data : [] + const hs: Household[] = list.map((h) => ({ id: h.id, name: h.name, slug: h.slug })) + setHouseholds(hs) + return hs + } + + return { user, households, activeHousehold, loadUser, login, loginWithPassword, logout, setHouseholds, setActiveHousehold, fetchHouseholds } } diff --git a/src/router/index.ts b/src/router/index.ts index 5c660d2..27d42fb 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -38,7 +38,7 @@ export function createAppRouter(getCurrentUser: () => Promise | unknown const routes: RouteRecordRaw[] = [] if (multitenantEnabled) { - routes.push({ path: '/', redirect: { name: 'login' } }) + routes.push({ path: '/', name: 'root', component: { template: '
' }, meta: { requiresAuth: true } }) routes.push(...publicRoutes) routes.push({ path: '/:householdSlug', component: { template: '' }, children: featureChildren }) } else { @@ -64,10 +64,42 @@ export function createAppRouter(getCurrentUser: () => Promise | unknown }) router.beforeEach(async (to) => { + // Allow public routes + const publicNames = new Set(['login', 'create-account', 'welcome', 'invitation-accept']) + if (publicNames.has(String(to.name))) return true + if (!to.meta.requiresAuth) return true try { const user = await getCurrentUser() - if (user) return true + if (!user) throw new Error('not-authenticated') + if (!multitenantEnabled) return true + + // Multitenant redirects + // Fetch households and redirect accordingly + const { useAuth } = await import('@/composables/useAuth') + const { fetchHouseholds, setActiveHousehold, households } = useAuth() + const hs = households.value.length > 0 ? households.value : await fetchHouseholds() + + if (hs.length === 0) { + if (to.name !== 'welcome') return { name: 'welcome' } + return true + } + + // If no slug in route, redirect to first household's mealplan + const slug = typeof to.params.householdSlug === 'string' ? to.params.householdSlug : null + if (!slug) { + if (hs.length > 0) { + const first = hs[0]! + setActiveHousehold(first) + return { name: 'mealplan', params: { householdSlug: first.slug } } + } + // Fallback (should not reach here due to hs.length check) + return true + } + // Ensure active household is set when navigating to a household route + const found = hs.find((h) => h.slug === slug) ?? (hs.length > 0 ? hs[0] : null) + if (found) setActiveHousehold(found) + return true } catch (_) { /* ignore */ } diff --git a/tests/households.useAuth.test.ts b/tests/households.useAuth.test.ts new file mode 100644 index 0000000..d09bbc2 --- /dev/null +++ b/tests/households.useAuth.test.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from 'vitest' +import { server, http, HttpResponse } from './test-setup' +import { useAuth } from '@/composables/useAuth' + +describe('useAuth fetchHouseholds', () => { + it('loads households and stores them', async () => { + server.use( + http.get('*/api/v1/users/me/households', () => { + return HttpResponse.json([ + { id: 1, name: 'Smiths', slug: 'the-smiths' }, + { id: 2, name: 'Johnsons', slug: 'the-johnsons' }, + ]) + }) + ) + const { households, fetchHouseholds, setActiveHousehold, activeHousehold } = useAuth() + expect(households.value).toEqual([]) + const hs = await fetchHouseholds() + expect(hs.length).toBe(2) + expect(households.value[0].slug).toBe('the-smiths') + setActiveHousehold(hs[1]) + expect(activeHousehold.value?.slug).toBe('the-johnsons') + }) +})