- 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.
This commit is contained in:
jableader 2025-11-01 13:55:55 +11:00
parent 22174ebf79
commit 04dbddee44
4 changed files with 85 additions and 18 deletions

View file

@ -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.
---

View file

@ -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<Household[]> {
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 }
}

View file

@ -38,7 +38,7 @@ export function createAppRouter(getCurrentUser: () => Promise<unknown> | unknown
const routes: RouteRecordRaw[] = []
if (multitenantEnabled) {
routes.push({ path: '/', redirect: { name: 'login' } })
routes.push({ path: '/', name: 'root', component: { template: '<div />' }, meta: { requiresAuth: true } })
routes.push(...publicRoutes)
routes.push({ path: '/:householdSlug', component: { template: '<router-view />' }, children: featureChildren })
} else {
@ -64,10 +64,42 @@ export function createAppRouter(getCurrentUser: () => Promise<unknown> | 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 */
}

View file

@ -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')
})
})