From cf8fc7b7190c43eaa1c296e6e3a7bad676151c38 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Fri, 11 Sep 2026 12:42:35 -0500 Subject: [PATCH] Fix users unable to edit or delete their own addresses --- .../(user)/profile/+page.server.ts | 72 ++++++++ .../(protected)/(user)/profile/+page.svelte | 96 +++++++++- .../(user)/profile/page.server.spec.ts | 168 +++++++++++++++++- 3 files changed, 330 insertions(+), 6 deletions(-) diff --git a/src/routes/(protected)/(user)/profile/+page.server.ts b/src/routes/(protected)/(user)/profile/+page.server.ts index e6afe7d..b942591 100644 --- a/src/routes/(protected)/(user)/profile/+page.server.ts +++ b/src/routes/(protected)/(user)/profile/+page.server.ts @@ -90,5 +90,77 @@ export const actions: Actions = { console.error('Address create error:', err); return fail(500, { error: 'Failed to add address.', action: 'addAddress' }); } + }, + + updateAddress: async ({ request, locals }) => { + if (!locals.user?.id) { + throw error(403, 'not authorized'); + } + + const data = await request.formData(); + const id = data.get('id') as string; + const street = data.get('street') as string; + const city = data.get('city') as string; + const state = data.get('state') as string; + const postalCode = data.get('postalCode') as string; + const country = data.get('country') as string; + + if (!id) { + return fail(400, { error: 'Missing address id.', action: 'updateAddress' }); + } + + if (!street || !city || !state || !postalCode || !country) { + return fail(400, { error: 'All address fields are required.', action: 'updateAddress' }); + } + + const address = await prisma.address.findFirst({ + where: { id, profile: { userId: locals.user.id } }, + select: { id: true } + }); + + if (!address) { + throw error(404, 'Address not found'); + } + + try { + await prisma.address.update({ + where: { id }, + data: { street, city, state, postalCode, country } + }); + return { success: true, action: 'updateAddress' }; + } catch (err) { + console.error('Address update error:', err); + return fail(500, { error: 'Failed to update address.', action: 'updateAddress' }); + } + }, + + deleteAddress: async ({ request, locals }) => { + if (!locals.user?.id) { + throw error(403, 'not authorized'); + } + + const data = await request.formData(); + const id = data.get('id') as string; + + if (!id) { + return fail(400, { error: 'Missing address id.', action: 'deleteAddress' }); + } + + const address = await prisma.address.findFirst({ + where: { id, profile: { userId: locals.user.id } }, + select: { id: true } + }); + + if (!address) { + throw error(404, 'Address not found'); + } + + try { + await prisma.address.delete({ where: { id } }); + return { success: true, action: 'deleteAddress' }; + } catch (err) { + console.error('Address delete error:', err); + return fail(500, { error: 'Failed to delete address.', action: 'deleteAddress' }); + } } }; diff --git a/src/routes/(protected)/(user)/profile/+page.svelte b/src/routes/(protected)/(user)/profile/+page.svelte index 2f5095a..55b4fd5 100644 --- a/src/routes/(protected)/(user)/profile/+page.svelte +++ b/src/routes/(protected)/(user)/profile/+page.svelte @@ -16,6 +16,44 @@ }); let showAddressForm = $state(false); + let editingAddressId = $state(null); + let addrStreet = $state(''); + let addrCity = $state(''); + let addrState = $state(''); + let addrPostal = $state(''); + let addrCountry = $state('US'); + + function openAddAddress() { + editingAddressId = null; + addrStreet = ''; + addrCity = ''; + addrState = ''; + addrPostal = ''; + addrCountry = 'US'; + showAddressForm = true; + } + + function openEditAddress(address: { + id: string; + street: string; + city: string; + state: string; + postalCode: string; + country: string; + }) { + editingAddressId = address.id; + addrStreet = address.street; + addrCity = address.city; + addrState = address.state; + addrPostal = address.postalCode; + addrCountry = address.country; + showAddressForm = true; + } + + function closeAddressForm() { + showAddressForm = false; + editingAddressId = null; + } function handlePhoneInput(event: Event) { const input = event.target as HTMLInputElement; @@ -102,7 +140,7 @@

Addresses

@@ -198,6 +260,30 @@ {address.postalCode}

{address.country}

+
+ +
{ + if (!confirm('Delete this address?')) event.preventDefault(); + }} + > + + +
+
{/each} diff --git a/src/routes/(protected)/(user)/profile/page.server.spec.ts b/src/routes/(protected)/(user)/profile/page.server.spec.ts index 4d48099..50901ec 100644 --- a/src/routes/(protected)/(user)/profile/page.server.spec.ts +++ b/src/routes/(protected)/(user)/profile/page.server.spec.ts @@ -11,7 +11,7 @@ vi.mock('$lib/db', () => ({ prisma: { user: { findUnique: vi.fn() }, profile: { findFirst: vi.fn(), updateMany: vi.fn() }, - address: { create: vi.fn() } + address: { create: vi.fn(), findFirst: vi.fn(), update: vi.fn(), delete: vi.fn() } } })); @@ -22,6 +22,9 @@ const userFindUnique = prisma.user.findUnique as ReturnType; const profileFindFirst = prisma.profile.findFirst as ReturnType; const profileUpdateMany = prisma.profile.updateMany as ReturnType; const addressCreate = prisma.address.create as ReturnType; +const addressFindFirst = prisma.address.findFirst as ReturnType; +const addressUpdate = prisma.address.update as ReturnType; +const addressDelete = prisma.address.delete as ReturnType; const invokeAction = (name: string, request: Request, locals: App.Locals) => (actions as any)[name]({ request, locals } as any); @@ -32,6 +35,9 @@ describe('(protected)/profile page server', () => { profileFindFirst.mockReset(); profileUpdateMany.mockReset(); addressCreate.mockReset(); + addressFindFirst.mockReset(); + addressUpdate.mockReset(); + addressDelete.mockReset(); }); describe('load', () => { @@ -254,4 +260,164 @@ describe('(protected)/profile page server', () => { expect((result as any).status).toBe(500); }); }); + + describe('updateAddress action', () => { + it('throws 403 when locals.user is undefined', async () => { + const request = createFormRequest({ + id: 'addr-1', + street: '123 Main St', + city: 'Dallas', + state: 'TX', + postalCode: '75001', + country: 'US' + }); + + const err = await Promise.resolve( + invokeAction('updateAddress', request, createLocals()) + ).catch((e: unknown) => e); + expect(isHttpError(err, 403)).toBe(true); + }); + + it('returns fail(400) when id is missing', async () => { + const request = createFormRequest({ + id: '', + street: '123 Main St', + city: 'Dallas', + state: 'TX', + postalCode: '75001', + country: 'US' + }); + + const locals = createLocals(mockUser() as any); + const result = await invokeAction('updateAddress', request, locals); + expect(isActionFailure(result)).toBe(true); + expect((result as any).data.action).toBe('updateAddress'); + }); + + it('throws 404 when the address is not owned by the user', async () => { + addressFindFirst.mockResolvedValue(null); + + const request = createFormRequest({ + id: 'addr-1', + street: '123 Main St', + city: 'Dallas', + state: 'TX', + postalCode: '75001', + country: 'US' + }); + + const locals = createLocals(mockUser() as any); + const err = await Promise.resolve(invokeAction('updateAddress', request, locals)).catch( + (e: unknown) => e + ); + expect(isHttpError(err, 404)).toBe(true); + expect(addressFindFirst).toHaveBeenCalledWith({ + where: { id: 'addr-1', profile: { userId: 'user-1' } }, + select: { id: true } + }); + }); + + it('updates the owned address on success', async () => { + addressFindFirst.mockResolvedValue({ id: 'addr-1' }); + addressUpdate.mockResolvedValue({}); + + const request = createFormRequest({ + id: 'addr-1', + street: '456 Oak Ave', + city: 'Austin', + state: 'TX', + postalCode: '73301', + country: 'US' + }); + + const locals = createLocals(mockUser() as any); + const result = await invokeAction('updateAddress', request, locals); + expect(result).toEqual({ success: true, action: 'updateAddress' }); + expect(addressUpdate).toHaveBeenCalledWith({ + where: { id: 'addr-1' }, + data: { + street: '456 Oak Ave', + city: 'Austin', + state: 'TX', + postalCode: '73301', + country: 'US' + } + }); + }); + + it('returns fail(500) when prisma update throws', async () => { + addressFindFirst.mockResolvedValue({ id: 'addr-1' }); + addressUpdate.mockRejectedValue(new Error('db error')); + + const request = createFormRequest({ + id: 'addr-1', + street: '123 Main St', + city: 'Dallas', + state: 'TX', + postalCode: '75001', + country: 'US' + }); + + const locals = createLocals(mockUser() as any); + const result = await invokeAction('updateAddress', request, locals); + expect(isActionFailure(result)).toBe(true); + expect((result as any).status).toBe(500); + }); + }); + + describe('deleteAddress action', () => { + it('throws 403 when locals.user is undefined', async () => { + const request = createFormRequest({ id: 'addr-1' }); + + const err = await Promise.resolve( + invokeAction('deleteAddress', request, createLocals()) + ).catch((e: unknown) => e); + expect(isHttpError(err, 403)).toBe(true); + }); + + it('returns fail(400) when id is missing', async () => { + const request = createFormRequest({ id: '' }); + + const locals = createLocals(mockUser() as any); + const result = await invokeAction('deleteAddress', request, locals); + expect(isActionFailure(result)).toBe(true); + expect((result as any).data.action).toBe('deleteAddress'); + }); + + it('throws 404 when the address is not owned by the user', async () => { + addressFindFirst.mockResolvedValue(null); + + const request = createFormRequest({ id: 'addr-1' }); + + const locals = createLocals(mockUser() as any); + const err = await Promise.resolve(invokeAction('deleteAddress', request, locals)).catch( + (e: unknown) => e + ); + expect(isHttpError(err, 404)).toBe(true); + }); + + it('deletes the owned address on success', async () => { + addressFindFirst.mockResolvedValue({ id: 'addr-1' }); + addressDelete.mockResolvedValue({}); + + const request = createFormRequest({ id: 'addr-1' }); + + const locals = createLocals(mockUser() as any); + const result = await invokeAction('deleteAddress', request, locals); + expect(result).toEqual({ success: true, action: 'deleteAddress' }); + expect(addressDelete).toHaveBeenCalledWith({ where: { id: 'addr-1' } }); + }); + + it('returns fail(500) when prisma delete throws', async () => { + addressFindFirst.mockResolvedValue({ id: 'addr-1' }); + addressDelete.mockRejectedValue(new Error('db error')); + + const request = createFormRequest({ id: 'addr-1' }); + + const locals = createLocals(mockUser() as any); + const result = await invokeAction('deleteAddress', request, locals); + expect(isActionFailure(result)).toBe(true); + expect((result as any).status).toBe(500); + }); + }); });