Enforce roleId checks on all admin page actions

This commit is contained in:
NeilClack
2026-09-10 14:59:32 -05:00
parent 0cb4b2148a
commit 7596937f30
14 changed files with 217 additions and 112 deletions
@@ -1,5 +1,6 @@
import type { PageServerLoad } from './$types';
import { prisma } from '$lib/db';
import { isAdmin, isStaff } from '$lib/server/roles';
import { superValidate } from 'sveltekit-superforms/server';
import { zod4 } from 'sveltekit-superforms/adapters';
import { z } from 'zod';
@@ -45,7 +46,11 @@ export const load: PageServerLoad = async () => {
};
export const actions = {
upsert: async ({ request }) => {
upsert: async ({ request, locals }) => {
if (!isStaff(locals)) {
return { success: false, action: 'upsert', error: 'Not authorized' };
}
const formData = await request.formData();
const form = await superValidate(formData, zod4(ProfileSchema));
if (!form.valid) {
@@ -112,14 +117,22 @@ export const actions = {
}
return { success: true, action: 'upsert' };
},
delete: async ({ request }) => {
delete: async ({ request, locals }) => {
if (!isAdmin(locals)) {
return { success: false, action: 'delete', error: 'Not authorized' };
}
const formData = await request.formData();
const rawId = formData.get('id')?.toString();
if (!rawId) return { success: false, error: 'Missing id' };
await prisma.profile.delete({ where: { id: rawId } });
return { success: true, action: 'delete' };
},
upsertAddress: async ({ request }) => {
upsertAddress: async ({ request, locals }) => {
if (!isStaff(locals)) {
return { success: false, action: 'upsertAddress', addressError: 'Not authorized' };
}
const formData = await request.formData();
const form = await superValidate(formData, zod4(AddressSchema));
if (!form.valid) {
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createLocals, createFormRequest } from '$lib/test-utils';
import { createLocals, createFormRequest, mockUser } from '$lib/test-utils';
vi.mock('$lib/db', () => ({
prisma: {
@@ -41,6 +41,9 @@ function makeProfileRequest(overrides: Record<string, string> = {}) {
return createFormRequest({ ...validFields, ...overrides });
}
const staff = () => createLocals(mockUser({ roleId: 3 }) as any);
const admin = () => createLocals(mockUser({ roleId: 2 }) as any);
describe('profiles admin page server', () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -73,7 +76,10 @@ describe('profiles admin page server', () => {
mockUserFindUnique.mockResolvedValue({ id: 'user-1', email: validFields.email });
mockProfileCreate.mockResolvedValue({ id: 'p-new' });
const result = await actions.upsert({ request: makeProfileRequest() } as any);
const result = await actions.upsert({
request: makeProfileRequest(),
locals: staff()
} as any);
expect(result).toEqual({ success: true, action: 'upsert' });
expect(mockUserFindUnique).toHaveBeenCalledWith({ where: { email: validFields.email } });
@@ -86,7 +92,10 @@ describe('profiles admin page server', () => {
mockUserFindUnique.mockResolvedValue(null);
mockProfileCreate.mockResolvedValue({ id: 'p-new' });
const result = await actions.upsert({ request: makeProfileRequest() } as any);
const result = await actions.upsert({
request: makeProfileRequest(),
locals: staff()
} as any);
expect(result).toEqual({ success: true, action: 'upsert' });
expect(mockProfileCreate).toHaveBeenCalledWith({ data: validFields });
@@ -96,7 +105,10 @@ describe('profiles admin page server', () => {
mockUserFindUnique.mockResolvedValue(null);
mockProfileCreate.mockRejectedValue({ code: 'P2002' });
const result = await actions.upsert({ request: makeProfileRequest() } as any);
const result = await actions.upsert({
request: makeProfileRequest(),
locals: staff()
} as any);
expect(result).toEqual({
success: false,
@@ -119,7 +131,7 @@ describe('profiles admin page server', () => {
mockProfileUpdate.mockResolvedValue({});
const request = makeProfileRequest({ email: 'new@example.com', id: 'p1' });
const result = await actions.upsert({ request } as any);
const result = await actions.upsert({ request, locals: staff() } as any);
expect(result).toEqual({ success: true, action: 'upsert' });
expect(mockUserUpdate).toHaveBeenCalledWith({
@@ -142,7 +154,7 @@ describe('profiles admin page server', () => {
mockProfileUpdate.mockResolvedValue({});
const request = makeProfileRequest({ id: 'p1' });
const result = await actions.upsert({ request } as any);
const result = await actions.upsert({ request, locals: staff() } as any);
expect(result).toEqual({ success: true, action: 'upsert' });
expect(mockUserFindFirst).not.toHaveBeenCalled();
@@ -159,7 +171,7 @@ describe('profiles admin page server', () => {
mockUserFindFirst.mockResolvedValue({ id: 'user-2' });
const request = makeProfileRequest({ email: 'taken@example.com', id: 'p1' });
const result = await actions.upsert({ request } as any);
const result = await actions.upsert({ request, locals: staff() } as any);
expect(result).toEqual({
success: false,
@@ -181,7 +193,7 @@ describe('profiles admin page server', () => {
mockProfileUpdate.mockResolvedValue({});
const request = makeProfileRequest({ id: 'p1' });
const result = await actions.upsert({ request } as any);
const result = await actions.upsert({ request, locals: staff() } as any);
expect(result).toEqual({ success: true, action: 'upsert' });
expect(mockProfileUpdate).toHaveBeenCalledWith({
@@ -194,7 +206,7 @@ describe('profiles admin page server', () => {
describe('upsert - validation', () => {
it('returns field errors for invalid input', async () => {
const request = makeProfileRequest({ email: 'not-an-email' });
const result = await actions.upsert({ request } as any);
const result = await actions.upsert({ request, locals: staff() } as any);
expect(result.success).toBe(false);
expect(result.action).toBe('upsert');
@@ -205,7 +217,7 @@ describe('profiles admin page server', () => {
mockProfileFindUnique.mockResolvedValue(null);
const request = makeProfileRequest({ id: 'missing' });
const result = await actions.upsert({ request } as any);
const result = await actions.upsert({ request, locals: staff() } as any);
expect(result).toEqual({ success: false, action: 'upsert', error: 'Record not found' });
});
@@ -215,7 +227,7 @@ describe('profiles admin page server', () => {
it('deletes the profile by id', async () => {
mockProfileDelete.mockResolvedValue({});
const locals = createLocals();
const locals = admin();
const req = createFormRequest({ id: 'p1' });
const result = await actions.delete({ request: req, locals } as any);
@@ -224,7 +236,7 @@ describe('profiles admin page server', () => {
});
it('returns an error when id is missing', async () => {
const locals = createLocals();
const locals = admin();
const result = await actions.delete({ request: createFormRequest({}), locals } as any);
expect(result).toEqual({ success: false, error: 'Missing id' });