- 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**: 1. **[~] Refactor Authentication State & API**:
- **Modify `src/api/auth.ts`**: - **Modify `src/api/auth.ts`**:
- Added minimal multitenant-ready surface while preserving legacy login: - Implemented JWT-based login/register using new endpoints; Authorization header injected via client provider.
- `loginWithPassword(email, password)` implemented by delegating to legacy `login(username)`; tests added. - `loginWithPassword(email, password)` and `createAccount(email, displayName, password)` now return `User` and set token.
- `logout()` clears cached user; covered by tests. - `logout()` clears token and cached user.
- Stubs for `createAccount(email, displayName, password)` and `handleGoogleLogin(token)` throw pending-implementation errors; tests assert presence. - `currentUser()` remains a bridge to cookie-based refresh; adapts legacy `Person` to `User` temporarily.
- Token/JWT storage is still cookie-based (per current backend); no localStorage changes yet.
- **Modify `src/composables/useAuth.ts`**: - **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**: 2. **[~] Update Router for Multi-Tenancy**:
- **Modify `src/router/index.ts`**: - **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 enabled: feature routes are nested under `/:householdSlug/...`.
- When disabled: legacy flat routes remain for backward compatibility. - When disabled: legacy flat routes remain for backward compatibility.
- **Refactor the `beforeEach` guard**: - **Refactor the `beforeEach` guard**:
- It should allow access to public routes. - Allows public routes.
- After login, it must fetch the user's households. - When multitenant flag is on, after auth fetches households via `useAuth().fetchHouseholds()`.
- If the user has no households, redirect to `/welcome`. - If none: redirect to `/welcome`.
- If the user has households but is at the root (`/`), redirect to the first household's dashboard (e.g., `/${householdSlug}/dashboard`). - 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. - **Nest existing routes**: Implemented behind feature flag.
3. **[ ] Implement Onboarding and Invitation Flows**: 3. **[ ] Implement Onboarding and Invitation Flows**:
- Build the `Welcome.vue` view for creating the first household. - Build the `Welcome.vue` view for creating the first household.
- Build the `CreateAccount.vue` view. - 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. - 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**: 4. **[~] Integrate Household Context into the App**:
- **Create `src/composables/useHousehold.ts`**: Implemented. Extracts `householdSlug` from route and binds provider to API client. - **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`. - **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. - **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`. - Verified by `tests/household.header.test.ts` and auth tests.
5. **[ ] Implement Invitation UI**: 5. **[ ] Implement Invitation UI**:
- Build the `HouseholdSettings.vue` view for inviting members and listing current members. - 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. - 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. - 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. - 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. - Implemented `useHousehold.ts`, header + auth providers 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 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 { ref } from 'vue'
import { currentUser as apiCurrentUser, login as apiLogin, loginWithPassword as apiLoginWithPassword, logout as apiLogout } from '@/api/auth' 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' import type { User } from '@/domain/types'
type Household = { id: number; name: string; slug: string } type Household = { id: number; name: string; slug: string }
@ -51,5 +52,13 @@ export function setActiveHousehold(h: Household | string | null) {
} }
export function useAuth() { 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[] = [] const routes: RouteRecordRaw[] = []
if (multitenantEnabled) { if (multitenantEnabled) {
routes.push({ path: '/', redirect: { name: 'login' } }) routes.push({ path: '/', name: 'root', component: { template: '<div />' }, meta: { requiresAuth: true } })
routes.push(...publicRoutes) routes.push(...publicRoutes)
routes.push({ path: '/:householdSlug', component: { template: '<router-view />' }, children: featureChildren }) routes.push({ path: '/:householdSlug', component: { template: '<router-view />' }, children: featureChildren })
} else { } else {
@ -64,10 +64,42 @@ export function createAppRouter(getCurrentUser: () => Promise<unknown> | unknown
}) })
router.beforeEach(async (to) => { 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 if (!to.meta.requiresAuth) return true
try { try {
const user = await getCurrentUser() 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 (_) { } catch (_) {
/* ignore */ /* 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')
})
})