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}
+
+
+
+
{/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);
+ });
+ });
});