From b19051fc7599e93306ec57a19fc0e28b1340edf3 Mon Sep 17 00:00:00 2001 From: jableader Date: Sat, 1 Nov 2025 14:21:48 +1100 Subject: [PATCH] feat(invitations): accept and send invites; add settings view and routes --- src/api/invitations.ts | 9 ++++++ src/router/index.ts | 3 ++ src/views/HouseholdSettings.vue | 51 ++++++++++++++++++++++++++++++ src/views/InvitationAccept.vue | 11 +++++-- tests/invitations.send.api.test.ts | 34 ++++++++++++++++++++ 5 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 src/views/HouseholdSettings.vue create mode 100644 tests/invitations.send.api.test.ts diff --git a/src/api/invitations.ts b/src/api/invitations.ts index 6891315..df60230 100644 --- a/src/api/invitations.ts +++ b/src/api/invitations.ts @@ -16,3 +16,12 @@ export async function acceptInvitation(token: string): Promise { } return { id: h.id, name: h.name, slug: h.slug } } + +export async function sendInvitation(email: string): Promise { + const resp = await fetchApi('/api/v1/invitations', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }), + }) + if (!resp.ok && resp.status !== 204) throw new Error(`${resp.status} ${resp.statusText || 'HTTP error'}`) +} diff --git a/src/router/index.ts b/src/router/index.ts index 27d42fb..ffb18fe 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -13,6 +13,7 @@ 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') +const HouseholdSettings = () => import('@/views/HouseholdSettings.vue') export function createAppRouter(getCurrentUser: () => Promise | unknown): Router { const multitenantEnabled = typeof process !== 'undefined' && process.env?.VUE_APP_MULTITENANT_ENABLED === 'true' @@ -33,6 +34,7 @@ export function createAppRouter(getCurrentUser: () => Promise | unknown { 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: 'settings/members', name: 'household-settings', component: HouseholdSettings, meta: { requiresAuth: true } }, ] const routes: RouteRecordRaw[] = [] @@ -55,6 +57,7 @@ export function createAppRouter(getCurrentUser: () => Promise | unknown { 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: '/settings/members', name: 'household-settings', component: HouseholdSettings, meta: { requiresAuth: true } }, ) } diff --git a/src/views/HouseholdSettings.vue b/src/views/HouseholdSettings.vue new file mode 100644 index 0000000..c961965 --- /dev/null +++ b/src/views/HouseholdSettings.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/src/views/InvitationAccept.vue b/src/views/InvitationAccept.vue index 45eba25..9838cef 100644 --- a/src/views/InvitationAccept.vue +++ b/src/views/InvitationAccept.vue @@ -1,8 +1,15 @@ diff --git a/tests/invitations.send.api.test.ts b/tests/invitations.send.api.test.ts new file mode 100644 index 0000000..5e84dfe --- /dev/null +++ b/tests/invitations.send.api.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect } from 'vitest' +import { server, http, HttpResponse } from './test-setup' +import { loginWithPassword } from '@/api/auth' +import { sendInvitation } from '@/api/invitations' +import { setHouseholdSlugProvider } from '@/api/client' + +describe('invitations api (send invite)', () => { + it('posts email with Authorization and X-Household-Slug', async () => { + // Simulate auth token + server.use( + http.post('*/api/v1/auth/login', () => + HttpResponse.json({ accessToken: 'tokABC', tokenType: 'bearer', user: { id: 4, email: 'x@y', displayName: 'X' } }) + ) + ) + await loginWithPassword('x@y', 'pw') + + // Provide a household slug for header injection + setHouseholdSlugProvider(() => 'the-smiths') + + server.use( + http.post('*/api/v1/invitations', async ({ request }) => { + const body = await request.json() + expect(body).toEqual({ email: 'invite@example.com' }) + const auth = request.headers.get('authorization') + expect(auth?.toLowerCase()).toBe('bearer tokabc') + const slug = request.headers.get('x-household-slug') + expect(slug).toBe('the-smiths') + return HttpResponse.json({}, { status: 204 }) + }) + ) + + await expect(sendInvitation('invite@example.com')).resolves.toBeUndefined() + }) +})