Fix staff(+) unable to edit or delete addresses
This commit is contained in:
@@ -22,6 +22,7 @@ const ProfileSchema = z.object({
|
||||
});
|
||||
|
||||
const AddressSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
profileId: z.string().min(1),
|
||||
street: z.string().min(1, 'Street is required'),
|
||||
city: z.string().min(1, 'City is required'),
|
||||
@@ -147,8 +148,14 @@ export const actions = {
|
||||
};
|
||||
}
|
||||
|
||||
const { profileId, isDefaultMailing, isDefaultBilling, isDefaultShipping, ...addressData } =
|
||||
form.data;
|
||||
const {
|
||||
id,
|
||||
profileId,
|
||||
isDefaultMailing,
|
||||
isDefaultBilling,
|
||||
isDefaultShipping,
|
||||
...addressData
|
||||
} = form.data;
|
||||
|
||||
if (!profileId) {
|
||||
return { success: false, action: 'upsertAddress', addressError: 'Missing profile ID' };
|
||||
@@ -181,7 +188,36 @@ export const actions = {
|
||||
isDefaultShipping
|
||||
};
|
||||
|
||||
if (id) {
|
||||
const existing = await prisma.address.findFirst({ where: { id, profileId } });
|
||||
if (!existing) {
|
||||
return { success: false, action: 'upsertAddress', addressError: 'Address not found' };
|
||||
}
|
||||
const updated = await prisma.address.update({ where: { id }, data: payload });
|
||||
return { success: true, action: 'upsertAddress', address: updated };
|
||||
}
|
||||
|
||||
const created = await prisma.address.create({ data: payload });
|
||||
return { success: true, action: 'upsertAddress', address: created };
|
||||
},
|
||||
deleteAddress: async ({ request, locals }) => {
|
||||
if (!isStaff(locals)) {
|
||||
return { success: false, action: 'deleteAddress', addressError: 'Not authorized' };
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const id = formData.get('id')?.toString();
|
||||
if (!id) {
|
||||
return { success: false, action: 'deleteAddress', addressError: 'Missing address id' };
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.address.delete({ where: { id } });
|
||||
} catch (err) {
|
||||
console.error('Address delete error:', err);
|
||||
return { success: false, action: 'deleteAddress', addressError: 'Failed to delete address' };
|
||||
}
|
||||
|
||||
return { success: true, action: 'deleteAddress', addressId: id };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
fieldErrors?: Record<string, string>;
|
||||
addressError?: string;
|
||||
address?: Record<string, unknown>;
|
||||
addressId?: string;
|
||||
} | null;
|
||||
|
||||
let {
|
||||
@@ -32,6 +33,7 @@
|
||||
let addressError = $state('');
|
||||
let successMessage = $state('');
|
||||
let addressFormOpen = $state(false);
|
||||
let editingAddressId = $state<string | null>(null);
|
||||
let addresses = $state<Record<string, unknown>[]>([]);
|
||||
|
||||
let addrStreet = $state('');
|
||||
@@ -49,6 +51,7 @@
|
||||
let lastForm = $state.raw<ActionResult>(untrack(() => form ?? null));
|
||||
|
||||
function resetAddressFields() {
|
||||
editingAddressId = null;
|
||||
addrStreet = '';
|
||||
addrCity = '';
|
||||
addrState = '';
|
||||
@@ -89,7 +92,11 @@
|
||||
switch (form.action) {
|
||||
case 'upsertAddress':
|
||||
if (form.success && form.address) {
|
||||
addresses = [...addresses, form.address];
|
||||
const savedId = String(form.address.id ?? '');
|
||||
const exists = addresses.some((a) => String(a.id ?? '') === savedId);
|
||||
addresses = exists
|
||||
? addresses.map((a) => (String(a.id ?? '') === savedId ? form.address! : a))
|
||||
: [...addresses, form.address];
|
||||
addressFormOpen = false;
|
||||
addressError = '';
|
||||
successMessage = 'Address saved';
|
||||
@@ -99,6 +106,16 @@
|
||||
addressError = form.addressError ?? '';
|
||||
}
|
||||
break;
|
||||
case 'deleteAddress':
|
||||
if (form.success) {
|
||||
addresses = addresses.filter((a) => String(a.id ?? '') !== String(form.addressId ?? ''));
|
||||
addressError = '';
|
||||
successMessage = 'Address deleted';
|
||||
} else {
|
||||
successMessage = '';
|
||||
addressError = form.addressError ?? '';
|
||||
}
|
||||
break;
|
||||
case 'upsert':
|
||||
default:
|
||||
if (form.success) {
|
||||
@@ -119,6 +136,21 @@
|
||||
addressError = '';
|
||||
if (addressFormOpen) resetAddressFields();
|
||||
}
|
||||
|
||||
function openEditAddress(addr: Record<string, unknown>) {
|
||||
resetAddressFields();
|
||||
editingAddressId = String(addr.id ?? '');
|
||||
addrStreet = String(addr.street ?? '');
|
||||
addrCity = String(addr.city ?? '');
|
||||
addrState = String(addr.state ?? '');
|
||||
addrPostal = String(addr.postalCode ?? '');
|
||||
addrCountry = String(addr.country ?? '');
|
||||
addrMailing = Boolean(addr.isDefaultMailing);
|
||||
addrBilling = Boolean(addr.isDefaultBilling);
|
||||
addrShipping = Boolean(addr.isDefaultShipping);
|
||||
addressError = '';
|
||||
addressFormOpen = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<AdminModal title="Edit Profile" {open} {onClose} {canDelete} recordId={String(record.id ?? '')}>
|
||||
@@ -172,6 +204,9 @@
|
||||
{#if addressFormOpen}
|
||||
<form method="POST" action="?/upsertAddress" use:enhance class="mt-4 flex flex-col gap-4">
|
||||
<input type="hidden" name="profileId" value={String(record.id ?? '')} />
|
||||
{#if editingAddressId}
|
||||
<input type="hidden" name="id" value={editingAddressId} />
|
||||
{/if}
|
||||
{#if addressError}
|
||||
<div class="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{addressError}
|
||||
@@ -262,7 +297,7 @@
|
||||
type="submit"
|
||||
class="rounded-md bg-slate-800 px-4 py-2 text-sm font-medium text-white transition hover:bg-slate-700"
|
||||
>
|
||||
Save Address
|
||||
{editingAddressId ? 'Update Address' : 'Save Address'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -283,6 +318,7 @@
|
||||
<th scope="col" class="px-3 py-2">M</th>
|
||||
<th scope="col" class="px-3 py-2">B</th>
|
||||
<th scope="col" class="px-3 py-2">S</th>
|
||||
<th scope="col" class="px-3 py-2">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -296,6 +332,26 @@
|
||||
<td class="px-3 py-2 whitespace-nowrap">{addr.isDefaultMailing ? '✓' : ''}</td>
|
||||
<td class="px-3 py-2 whitespace-nowrap">{addr.isDefaultBilling ? '✓' : ''}</td>
|
||||
<td class="px-3 py-2 whitespace-nowrap">{addr.isDefaultShipping ? '✓' : ''}</td>
|
||||
<td class="px-3 py-2 whitespace-nowrap">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => openEditAddress(addr)}
|
||||
class="rounded-md border border-slate-300 bg-white px-2 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-50"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<form method="POST" action="?/deleteAddress" use:enhance>
|
||||
<input type="hidden" name="id" value={String(addr.id ?? '')} />
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md border border-red-300 bg-white px-2 py-1 text-xs font-medium text-red-600 transition hover:bg-red-50"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
|
||||
@@ -14,6 +14,12 @@ vi.mock('$lib/db', () => ({
|
||||
findUnique: vi.fn(),
|
||||
findFirst: vi.fn(),
|
||||
update: vi.fn()
|
||||
},
|
||||
address: {
|
||||
findFirst: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn()
|
||||
}
|
||||
}
|
||||
}));
|
||||
@@ -29,6 +35,10 @@ const mockProfileDelete = prisma.profile.delete as ReturnType<typeof vi.fn>;
|
||||
const mockUserFindUnique = prisma.user.findUnique as ReturnType<typeof vi.fn>;
|
||||
const mockUserFindFirst = prisma.user.findFirst as ReturnType<typeof vi.fn>;
|
||||
const mockUserUpdate = prisma.user.update as ReturnType<typeof vi.fn>;
|
||||
const mockAddressFindFirst = prisma.address.findFirst as ReturnType<typeof vi.fn>;
|
||||
const mockAddressCreate = prisma.address.create as ReturnType<typeof vi.fn>;
|
||||
const mockAddressUpdate = prisma.address.update as ReturnType<typeof vi.fn>;
|
||||
const mockAddressDelete = prisma.address.delete as ReturnType<typeof vi.fn>;
|
||||
|
||||
const validFields = {
|
||||
email: 'jane@example.com',
|
||||
@@ -242,4 +252,132 @@ describe('profiles admin page server', () => {
|
||||
expect(result).toEqual({ success: false, error: 'Missing id' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('upsertAddress', () => {
|
||||
const addressFields = {
|
||||
profileId: 'p1',
|
||||
street: '123 Main St',
|
||||
city: 'Dallas',
|
||||
state: 'TX',
|
||||
postalCode: '75001',
|
||||
country: 'US'
|
||||
};
|
||||
|
||||
it('rejects non-staff users', async () => {
|
||||
const result = await actions.upsertAddress({
|
||||
request: createFormRequest(addressFields),
|
||||
locals: createLocals(mockUser({ roleId: 4 }) as any)
|
||||
} as any);
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
action: 'upsertAddress',
|
||||
addressError: 'Not authorized'
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a new address when no id is supplied', async () => {
|
||||
const created = { id: 'a1', ...addressFields };
|
||||
mockAddressCreate.mockResolvedValue(created);
|
||||
|
||||
const result = await actions.upsertAddress({
|
||||
request: createFormRequest(addressFields),
|
||||
locals: staff()
|
||||
} as any);
|
||||
|
||||
expect(result).toEqual({ success: true, action: 'upsertAddress', address: created });
|
||||
expect(mockAddressCreate).toHaveBeenCalledWith({
|
||||
data: {
|
||||
profileId: 'p1',
|
||||
street: '123 Main St',
|
||||
city: 'Dallas',
|
||||
state: 'TX',
|
||||
postalCode: '75001',
|
||||
country: 'US',
|
||||
type: 'Mailing',
|
||||
isDefaultMailing: false,
|
||||
isDefaultBilling: false,
|
||||
isDefaultShipping: false
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('updates an existing address when an id is supplied', async () => {
|
||||
mockAddressFindFirst.mockResolvedValue({ id: 'a1' });
|
||||
const updated = { id: 'a1', ...addressFields, street: '456 Oak Ave' };
|
||||
mockAddressUpdate.mockResolvedValue(updated);
|
||||
|
||||
const result = await actions.upsertAddress({
|
||||
request: createFormRequest({ ...addressFields, id: 'a1', street: '456 Oak Ave' }),
|
||||
locals: staff()
|
||||
} as any);
|
||||
|
||||
expect(result).toEqual({ success: true, action: 'upsertAddress', address: updated });
|
||||
expect(mockAddressFindFirst).toHaveBeenCalledWith({
|
||||
where: { id: 'a1', profileId: 'p1' }
|
||||
});
|
||||
expect(mockAddressUpdate).toHaveBeenCalledWith({
|
||||
where: { id: 'a1' },
|
||||
data: expect.objectContaining({ street: '456 Oak Ave', profileId: 'p1' })
|
||||
});
|
||||
expect(mockAddressCreate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('errors when updating an address that does not belong to the profile', async () => {
|
||||
mockAddressFindFirst.mockResolvedValue(null);
|
||||
|
||||
const result = await actions.upsertAddress({
|
||||
request: createFormRequest({ ...addressFields, id: 'a1' }),
|
||||
locals: staff()
|
||||
} as any);
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
action: 'upsertAddress',
|
||||
addressError: 'Address not found'
|
||||
});
|
||||
expect(mockAddressUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteAddress', () => {
|
||||
it('rejects non-staff users', async () => {
|
||||
const result = await actions.deleteAddress({
|
||||
request: createFormRequest({ id: 'a1' }),
|
||||
locals: createLocals(mockUser({ roleId: 4 }) as any)
|
||||
} as any);
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
action: 'deleteAddress',
|
||||
addressError: 'Not authorized'
|
||||
});
|
||||
expect(mockAddressDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('deletes the address by id for staff', async () => {
|
||||
mockAddressDelete.mockResolvedValue({});
|
||||
|
||||
const result = await actions.deleteAddress({
|
||||
request: createFormRequest({ id: 'a1' }),
|
||||
locals: staff()
|
||||
} as any);
|
||||
|
||||
expect(result).toEqual({ success: true, action: 'deleteAddress', addressId: 'a1' });
|
||||
expect(mockAddressDelete).toHaveBeenCalledWith({ where: { id: 'a1' } });
|
||||
});
|
||||
|
||||
it('returns an error when id is missing', async () => {
|
||||
const result = await actions.deleteAddress({
|
||||
request: createFormRequest({}),
|
||||
locals: staff()
|
||||
} as any);
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
action: 'deleteAddress',
|
||||
addressError: 'Missing address id'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user