diff --git a/frontend-spec.md b/frontend-spec.md
index c4b800a..b814085 100644
--- a/frontend-spec.md
+++ b/frontend-spec.md
@@ -57,37 +57,40 @@ This plan is adapted to the existing codebase, focusing on refactoring rather th
## 4. Actionable Implementation Steps
-1. **[ ] Refactor Authentication State & API**:
+1. **[~] Refactor Authentication State & API**:
- **Modify `src/api/auth.ts`**:
- - Replace `login(username: string)` with `login(email, password)`.
- - Add `createAccount(email, displayName, password)`, `handleGoogleLogin(token)`, and `logout()`.
- - The API client should handle storing and clearing the auth token (e.g., from `localStorage`).
+ - 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.
- **Modify `src/composables/useAuth.ts`**:
- - Update the `login` function to accept email/password.
- - Add a `register` function.
- - The `user` ref should now hold the global User profile, and you should add a new state for `households` and `activeHousehold`.
- - The `logout` function should clear the JWT and all user state.
+ - TODO: Update to accept email/password and expose `register`, `logout`, `households`, and `activeHousehold`. Not started.
-2. **[ ] Update Router for Multi-Tenancy**:
+2. **[~] Update Router for Multi-Tenancy**:
- **Modify `src/router/index.ts`**:
- - Add new public routes: `/create-account`, `/welcome`, and `/invitations/accept`.
+ - 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**:
- 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`).
- - **Nest existing routes**: All current data-related routes (`/meals`, `/shopping`, etc.) must be moved as children of a new dynamic `/:householdSlug` route.
+ - **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.
-4. **[ ] Integrate Household Context into the App**:
- - **Create `src/composables/useHousehold.ts`**: This composable should extract the `householdSlug` from the current route's params. It will provide the `activeHouseholdSlug` to any component or service that needs it.
- - **Implement `HouseholdSwitcher.vue`**: This component will use `useAuth` to get the list of the user's households and render navigation links.
- - **Update API Services**: All data-fetching calls (e.g., for recipes, meals) must be updated to use the `activeHouseholdSlug` from `useHousehold`. The API client wrapper should be modified to prepend this slug to the request URL.
- - Example: `api.get('/recipes')` becomes `api.get(\`/${activeHouseholdSlug.value}/recipes\`)`.
+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`.
5. **[ ] Implement Invitation UI**:
- Build the `HouseholdSettings.vue` view for inviting members and listing current members.
@@ -119,11 +122,11 @@ What exists today
Gaps vs requirements
- Authentication & onboarding
- - Missing email/password login, Google OAuth, account creation, logout, JWT storage/refresh.
+ - Partial: Added `loginWithPassword` shim and `logout`. Still missing Google OAuth, account creation, JWT storage/refresh beyond cookie.
- Missing `CreateAccount.vue`, `Welcome.vue`, invitation acceptance flow.
- Routing & URL-based tenancy
- - Missing `/:householdSlug/...` route nesting and post-login redirect logic.
- - No `useHousehold.ts`; no `HouseholdSwitcher.vue`.
+ - 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.
- API boundary
- No household scoping passed to backend. Need a typed strategy (prefer header parameter) without breaking OpenAPI typing.
- Cleanup
@@ -144,6 +147,18 @@ To align with OpenAPI typing and README axioms, do not rewrite request paths to
Action
- Implement header injection in `api/client.ts` with a pluggable getter for the active slug (decoupled from Vue imports). Update this spec once backend finalizes the parameter shape.
+ - DONE: Implemented with `setHouseholdSlugProvider`. Will align to OpenAPI when backend finalizes.
+
+---
+
+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 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`.
---
diff --git a/src/App.vue b/src/App.vue
index e96fb13..9368ecd 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -29,6 +29,7 @@
+
@@ -39,6 +40,11 @@
diff --git a/src/api/auth.ts b/src/api/auth.ts
index d211e77..f1e9db6 100644
--- a/src/api/auth.ts
+++ b/src/api/auth.ts
@@ -28,3 +28,24 @@ export async function login(username: string): Promise
{
if (!cachedUser) throw new Error('Login failed: empty response')
return cachedUser
}
+
+// New multitenant-ready API surface (backward compatible)
+export async function loginWithPassword(email: string, _password: string): Promise {
+ // Backend currently expects { username }; map email to username until OpenAPI updates
+ return login(email)
+}
+
+export async function createAccount(_email: string, _displayName: string, _password: string): Promise {
+ // Placeholder until backend endpoints and OpenAPI are finalized
+ throw new Error('createAccount not implemented yet')
+}
+
+export async function handleGoogleLogin(_token: string): Promise {
+ // Placeholder until Google OAuth flow is wired
+ throw new Error('handleGoogleLogin not implemented yet')
+}
+
+export async function logout(): Promise {
+ // Clear local cache; server session is cookie-based and will be refreshed on next call
+ cachedUser = null
+}
diff --git a/src/api/client.ts b/src/api/client.ts
index c3d81de..b214122 100644
--- a/src/api/client.ts
+++ b/src/api/client.ts
@@ -9,12 +9,22 @@ const isTest = typeof process !== 'undefined' && (process.env?.VITEST === 'true'
const defaultBase = ''
const baseUrl: string = isTest ? 'http://localhost' : (vueCliBase || defaultBase)
+let householdSlugProvider: (() => string | null) | null = null
+
+export function setHouseholdSlugProvider(provider: (() => string | null) | null) {
+ householdSlugProvider = provider
+}
+
export const api = createClient({
baseUrl,
fetch: (input: RequestInfo | URL, init?: RequestInit) => {
+ const headers = new Headers(init?.headers || {})
+ const slug = householdSlugProvider ? householdSlugProvider() : null
+ if (slug) headers.set('X-Household-Slug', slug)
return globalThis.fetch(input, {
credentials: 'include',
...init,
+ headers,
})
},
})
diff --git a/src/components/HouseholdSwitcher.vue b/src/components/HouseholdSwitcher.vue
new file mode 100644
index 0000000..f2b0213
--- /dev/null
+++ b/src/components/HouseholdSwitcher.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
diff --git a/src/composables/useAuth.ts b/src/composables/useAuth.ts
index c6cdeca..139839c 100644
--- a/src/composables/useAuth.ts
+++ b/src/composables/useAuth.ts
@@ -1,8 +1,12 @@
import { ref } from 'vue'
-import { currentUser as apiCurrentUser, login as apiLogin } from '@/api/auth'
+import { currentUser as apiCurrentUser, login as apiLogin, loginWithPassword as apiLoginWithPassword, logout as apiLogout } from '@/api/auth'
import type { Person } from '@/domain/types'
+type Household = { id: number; name: string; slug: string }
+
const user = ref(null)
+const households = ref([])
+const activeHousehold = ref(null)
let initialized = false
export async function loadUser() {
@@ -18,6 +22,34 @@ export async function login(username: string) {
return user.value
}
-export function useAuth() {
- return { user, loadUser, login }
+export async function loginWithPassword(email: string, password: string) {
+ user.value = await apiLoginWithPassword(email, password)
+ return user.value
+}
+
+export async function logout() {
+ await apiLogout()
+ user.value = null
+ households.value = []
+ activeHousehold.value = null
+}
+
+export function setHouseholds(hs: Household[]) {
+ households.value = Array.isArray(hs) ? hs.slice() : []
+}
+
+export function setActiveHousehold(h: Household | string | null) {
+ if (h == null) {
+ activeHousehold.value = null
+ return
+ }
+ if (typeof h === 'string') {
+ activeHousehold.value = households.value.find((x) => x.slug === h) ?? null
+ } else {
+ activeHousehold.value = h
+ }
+}
+
+export function useAuth() {
+ return { user, households, activeHousehold, loadUser, login, loginWithPassword, logout, setHouseholds, setActiveHousehold }
}
diff --git a/src/composables/useHousehold.ts b/src/composables/useHousehold.ts
new file mode 100644
index 0000000..088e99a
--- /dev/null
+++ b/src/composables/useHousehold.ts
@@ -0,0 +1,18 @@
+import { computed, watchEffect } from 'vue'
+import { useRoute } from 'vue-router'
+import { setHouseholdSlugProvider } from '@/api/client'
+
+export function useHousehold() {
+ const route = useRoute()
+ const activeHouseholdSlug = computed(() => {
+ const p = route.params?.householdSlug
+ return typeof p === 'string' ? p : null
+ })
+
+ // Keep API client in sync with current route's household slug
+ watchEffect(() => {
+ setHouseholdSlugProvider(() => activeHouseholdSlug.value)
+ })
+
+ return { activeHouseholdSlug }
+}
diff --git a/src/router/index.ts b/src/router/index.ts
index ffa0276..5c660d2 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -10,55 +10,54 @@ const PurchasedShoppingListPage = () => import('@/components/shopping/PurchasedS
const CurrentShoppingListPage = () => import('@/components/shopping/CurrentShoppingListPage.vue')
const EditMealPage = () => import('@/components/meals/EditMealPage.vue')
const EditRecipePage = () => import('@/components/recipes/EditRecipePage.vue')
+const CreateAccount = () => import('@/views/CreateAccount.vue')
+const Welcome = () => import('@/views/Welcome.vue')
+const InvitationAccept = () => import('@/views/InvitationAccept.vue')
export function createAppRouter(getCurrentUser: () => Promise | unknown): Router {
- const routes: RouteRecordRaw[] = [
- { path: '/', redirect: { name: 'mealplan' } },
+ const multitenantEnabled = typeof process !== 'undefined' && process.env?.VUE_APP_MULTITENANT_ENABLED === 'true'
+ const publicRoutes: RouteRecordRaw[] = [
{ path: '/login', name: 'login', component: LoginPage },
- { path: '/mealplan', name: 'mealplan', component: MealPlanPage, meta: { requiresAuth: true } },
- {
- path: '/shopping',
- name: 'shopping',
- component: MyShoppingPage,
- meta: { requiresAuth: true },
- },
- {
- path: '/shopping/current',
- name: 'shopping-current',
- component: CurrentShoppingListPage,
- meta: { requiresAuth: true },
- },
- {
- path: '/shopping/:id',
- name: 'shopping-list',
- component: PurchasedShoppingListPage,
- props: true,
- meta: { requiresAuth: true },
- },
- { path: '/recipes', name: 'recipes', component: RecipesPage, meta: { requiresAuth: true } },
- {
- path: '/recipes/add',
- name: 'recipe-add',
- component: EditRecipePage,
- meta: { requiresAuth: true },
- },
- {
- path: '/recipes/:id',
- name: 'recipe-edit',
- component: EditRecipePage,
- props: true,
- meta: { requiresAuth: true },
- },
- { path: '/meals/add', name: 'meal-add', component: EditMealPage, meta: { requiresAuth: true } },
- {
- path: '/meals/:id',
- name: 'meal-edit',
- component: EditMealPage,
- props: true,
- meta: { requiresAuth: true },
- },
+ { path: '/create-account', name: 'create-account', component: CreateAccount },
+ { path: '/welcome', name: 'welcome', component: Welcome },
+ { path: '/invitations/accept', name: 'invitation-accept', component: InvitationAccept },
]
+ const featureChildren: RouteRecordRaw[] = [
+ { path: 'recipes', name: 'recipes', component: RecipesPage, meta: { requiresAuth: true } },
+ { path: 'recipes/add', name: 'recipe-add', component: EditRecipePage, meta: { requiresAuth: true } },
+ { path: 'recipes/:id', name: 'recipe-edit', component: EditRecipePage, props: true, meta: { requiresAuth: true } },
+ { path: 'mealplan', name: 'mealplan', component: MealPlanPage, meta: { requiresAuth: true } },
+ { path: 'meals/add', name: 'meal-add', component: EditMealPage, meta: { requiresAuth: true } },
+ { path: 'meals/:id', name: 'meal-edit', component: EditMealPage, props: true, meta: { requiresAuth: true } },
+ { path: 'shopping', name: 'shopping', component: MyShoppingPage, meta: { requiresAuth: true } },
+ { path: 'shopping/current', name: 'shopping-current', component: CurrentShoppingListPage, meta: { requiresAuth: true } },
+ { path: 'shopping/:id', name: 'shopping-list', component: PurchasedShoppingListPage, props: true, meta: { requiresAuth: true } },
+ ]
+
+ const routes: RouteRecordRaw[] = []
+
+ if (multitenantEnabled) {
+ routes.push({ path: '/', redirect: { name: 'login' } })
+ routes.push(...publicRoutes)
+ routes.push({ path: '/:householdSlug', component: { template: '' }, children: featureChildren })
+ } else {
+ routes.push({ path: '/', redirect: { name: 'mealplan' } })
+ routes.push(...publicRoutes)
+ // legacy flat routes
+ routes.push(
+ { path: '/recipes', name: 'recipes', component: RecipesPage, meta: { requiresAuth: true } },
+ { path: '/recipes/add', name: 'recipe-add', component: EditRecipePage, meta: { requiresAuth: true } },
+ { path: '/recipes/:id', name: 'recipe-edit', component: EditRecipePage, props: true, meta: { requiresAuth: true } },
+ { path: '/mealplan', name: 'mealplan', component: MealPlanPage, meta: { requiresAuth: true } },
+ { path: '/meals/add', name: 'meal-add', component: EditMealPage, meta: { requiresAuth: true } },
+ { path: '/meals/:id', name: 'meal-edit', component: EditMealPage, props: true, meta: { requiresAuth: true } },
+ { path: '/shopping', name: 'shopping', component: MyShoppingPage, meta: { requiresAuth: true } },
+ { path: '/shopping/current', name: 'shopping-current', component: CurrentShoppingListPage, meta: { requiresAuth: true } },
+ { path: '/shopping/:id', name: 'shopping-list', component: PurchasedShoppingListPage, props: true, meta: { requiresAuth: true } },
+ )
+ }
+
const router = createRouter({
history: createWebHashHistory(),
routes,
diff --git a/src/views/CreateAccount.vue b/src/views/CreateAccount.vue
new file mode 100644
index 0000000..aeb7001
--- /dev/null
+++ b/src/views/CreateAccount.vue
@@ -0,0 +1,10 @@
+
+
+
Create Account
+
Placeholder for email, display name, password, and Google signup.
+
+
+
+
diff --git a/src/views/InvitationAccept.vue b/src/views/InvitationAccept.vue
new file mode 100644
index 0000000..67f0d6e
--- /dev/null
+++ b/src/views/InvitationAccept.vue
@@ -0,0 +1,10 @@
+
+
+
Accept Invitation
+
Processing your invitation token...
+
+
+
+
diff --git a/src/views/Welcome.vue b/src/views/Welcome.vue
new file mode 100644
index 0000000..9facc91
--- /dev/null
+++ b/src/views/Welcome.vue
@@ -0,0 +1,10 @@
+
+
+
Welcome
+
Choose to create a new household or join via invitation link.
+
+
+
+
diff --git a/tests/auth.api.test.ts b/tests/auth.api.test.ts
new file mode 100644
index 0000000..451ec1d
--- /dev/null
+++ b/tests/auth.api.test.ts
@@ -0,0 +1,42 @@
+import { describe, it, expect } from 'vitest'
+import { server, http, HttpResponse } from './test-setup'
+import { currentUser, logout, loginWithPassword, createAccount, handleGoogleLogin } from '@/api/auth'
+
+describe('auth api (multitenant prep)', () => {
+ it('loginWithPassword proxies to /auth/login with username=email', async () => {
+ server.use(
+ http.post('*/api/v1/auth/login', async ({ request }) => {
+ const body = await request.json()
+ expect(body).toEqual({ username: 'test@example.com' })
+ return HttpResponse.json({ id: 123, name: 'Test User' })
+ })
+ )
+ const person = await loginWithPassword('test@example.com', 'secret')
+ expect(person.id).toBe(123)
+ expect(person.name).toBe('Test User')
+ })
+
+ it('logout clears cached user; subsequent currentUser returns null', async () => {
+ // First refresh returns a user
+ server.use(
+ http.post('*/api/v1/auth/refresh', () => {
+ return HttpResponse.json({ id: 1, name: 'Ada' })
+ })
+ )
+ const first = await currentUser()
+ expect(first?.name).toBe('Ada')
+
+ // After logout, next refresh returns 401 and currentUser should resolve to null
+ await logout()
+ server.use(
+ http.post('*/api/v1/auth/refresh', () => new HttpResponse(null, { status: 401 }))
+ )
+ const second = await currentUser()
+ expect(second).toBeNull()
+ })
+
+ it('createAccount and handleGoogleLogin exist but are not implemented yet', async () => {
+ await expect(createAccount('new@example.com', 'New User', 'pw')).rejects.toBeInstanceOf(Error)
+ await expect(handleGoogleLogin('token-123')).rejects.toBeInstanceOf(Error)
+ })
+})
diff --git a/tests/household.header.test.ts b/tests/household.header.test.ts
new file mode 100644
index 0000000..8b7a3d9
--- /dev/null
+++ b/tests/household.header.test.ts
@@ -0,0 +1,23 @@
+import { describe, it, expect } from 'vitest'
+import { server, http, HttpResponse } from './test-setup'
+import { api } from '@/api/client'
+
+describe('API household header injection', () => {
+ it('sends X-Household-Slug when provider returns slug', async () => {
+ server.use(
+ http.get('*/api/v1/recipes', ({ request }) => {
+ // Should include our header set by client provider
+ const slug = request.headers.get('x-household-slug') || request.headers.get('X-Household-Slug')
+ if (!slug) return new HttpResponse(null, { status: 400 })
+ return HttpResponse.json([])
+ })
+ )
+
+ // Directly set provider without Vue router by calling internal setter via dynamic import
+ const { setHouseholdSlugProvider } = await import('@/api/client')
+ setHouseholdSlugProvider(() => 'the-smiths')
+
+ const res = await api.GET('/api/v1/recipes', { params: {} })
+ expect(res.response.ok).toBe(true)
+ })
+})
diff --git a/tests/router.multitenant.test.ts b/tests/router.multitenant.test.ts
new file mode 100644
index 0000000..14750fc
--- /dev/null
+++ b/tests/router.multitenant.test.ts
@@ -0,0 +1,36 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+
+describe('router multitenant routing', () => {
+ beforeEach(() => {
+ vi.resetModules()
+ delete (process.env as any).VUE_APP_MULTITENANT_ENABLED
+ })
+
+ it('includes public routes and legacy flat routes when flag disabled', async () => {
+ const { createAppRouter } = await import('@/router/index')
+ const router = createAppRouter(() => null)
+ const paths = router.getRoutes().map((r) => r.path)
+ expect(paths).toContain('/login')
+ expect(paths).toContain('/create-account')
+ expect(paths).toContain('/welcome')
+ expect(paths).toContain('/invitations/accept')
+ // legacy flat routes
+ expect(paths).toContain('/recipes')
+ expect(paths).toContain('/shopping')
+ })
+
+ it('nests routes under /:householdSlug when flag enabled', async () => {
+ ;(process.env as any).VUE_APP_MULTITENANT_ENABLED = 'true'
+ const { createAppRouter } = await import('@/router/index')
+ const router = createAppRouter(() => null)
+ const paths = router.getRoutes().map((r) => r.path)
+ // Public routes still exist
+ expect(paths).toContain('/login')
+ expect(paths).toContain('/create-account')
+ // Nested route example
+ const hasNestedRecipes = paths.some((p) => p === '/:householdSlug/recipes')
+ expect(hasNestedRecipes).toBe(true)
+ // Legacy route should not be present when nested is enabled
+ expect(paths).not.toContain('/recipes')
+ })
+})
diff --git a/tests/useAuth.multitenant.test.ts b/tests/useAuth.multitenant.test.ts
new file mode 100644
index 0000000..7f0c806
--- /dev/null
+++ b/tests/useAuth.multitenant.test.ts
@@ -0,0 +1,62 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+
+vi.mock('@/api/auth', () => {
+ return {
+ currentUser: vi.fn(async () => ({ id: 42, name: 'Grace Hopper' })),
+ login: vi.fn(async (username: string) => ({ id: 7, name: username })),
+ loginWithPassword: vi.fn(async (email: string) => ({ id: 8, name: email })),
+ logout: vi.fn(async () => {}),
+ createAccount: vi.fn(async () => {
+ throw new Error('createAccount not implemented yet')
+ }),
+ handleGoogleLogin: vi.fn(async () => {
+ throw new Error('handleGoogleLogin not implemented yet')
+ }),
+ }
+})
+
+import { useAuth, loadUser } from '@/composables/useAuth'
+
+describe('useAuth (multitenant state)', () => {
+ beforeEach(() => {
+ // Reset modules and state between tests
+ vi.resetModules()
+ })
+
+ it('exposes households and activeHousehold state with setters', async () => {
+ const { user, households, activeHousehold, setHouseholds, setActiveHousehold } = useAuth()
+ expect(user.value).toBeNull()
+ expect(households.value).toEqual([])
+ expect(activeHousehold.value).toBeNull()
+
+ const hs = [
+ { id: 1, name: 'Smiths', slug: 'the-smiths' },
+ { id: 2, name: 'Johnsons', slug: 'the-johnsons' },
+ ]
+ setHouseholds(hs)
+ expect(households.value.length).toBe(2)
+ setActiveHousehold(hs[1])
+ expect(activeHousehold.value?.slug).toBe('the-johnsons')
+ setActiveHousehold('the-smiths')
+ expect(activeHousehold.value?.slug).toBe('the-smiths')
+ })
+
+ it('supports loading current user, loginWithPassword, and logout clearing user', async () => {
+ const { user, loginWithPassword, logout } = useAuth()
+ // load current user
+ await loadUser()
+ expect(user.value?.name).toBe('Grace Hopper')
+ // login with email/password
+ await loginWithPassword('user@example.com', 'pw')
+ expect(user.value?.name).toBe('user@example.com')
+ // logout clears user
+ await logout()
+ expect(user.value).toBeNull()
+ })
+
+ it('keeps legacy login(username) available for now', async () => {
+ const { user, login } = useAuth()
+ await login('legacy-user')
+ expect(user.value?.name).toBe('legacy-user')
+ })
+})