Enforce roleId checks on all admin page actions
This commit is contained in:
@@ -2,12 +2,10 @@ import type { PageServerLoad } from './$types';
|
|||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import { prisma } from '$lib/db';
|
import { prisma } from '$lib/db';
|
||||||
import { rotateApiKey } from '$lib/server/api-key';
|
import { rotateApiKey } from '$lib/server/api-key';
|
||||||
|
import { isSuper } from '$lib/server/roles';
|
||||||
|
|
||||||
const SUPER = 1;
|
export const load: PageServerLoad = async ({ locals }) => {
|
||||||
|
if (!isSuper(locals)) {
|
||||||
export const load: PageServerLoad = async ({ parent }) => {
|
|
||||||
const { adminUser } = await parent();
|
|
||||||
if (adminUser.roleId > SUPER) {
|
|
||||||
throw error(403, 'Only Super admins can manage API keys');
|
throw error(403, 'Only Super admins can manage API keys');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,18 +37,9 @@ export const load: PageServerLoad = async ({ parent }) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
async function viewerRoleId(locals: App.Locals): Promise<number> {
|
|
||||||
if (!locals.user?.id) return 4;
|
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: { id: locals.user.id },
|
|
||||||
select: { roleId: true }
|
|
||||||
});
|
|
||||||
return user?.roleId ?? 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
rotate: async ({ locals }) => {
|
rotate: async ({ locals }) => {
|
||||||
if ((await viewerRoleId(locals)) > SUPER) {
|
if (!isSuper(locals)) {
|
||||||
return { success: false, error: 'Not authorized' };
|
return { success: false, error: 'Not authorized' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +52,7 @@ export const actions = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
rotateEmergency: async ({ locals }) => {
|
rotateEmergency: async ({ locals }) => {
|
||||||
if ((await viewerRoleId(locals)) > SUPER) {
|
if (!isSuper(locals)) {
|
||||||
return { success: false, error: 'Not authorized' };
|
return { success: false, error: 'Not authorized' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import { prisma } from '$lib/db';
|
import { prisma } from '$lib/db';
|
||||||
import { sendMail } from '$lib/server/resend';
|
import { sendMail } from '$lib/server/resend';
|
||||||
|
import { isStaff } from '$lib/server/roles';
|
||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
function generateLicenseKey(): string {
|
function generateLicenseKey(): string {
|
||||||
@@ -18,7 +19,7 @@ export const load: PageServerLoad = async () => {
|
|||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
createKey: async ({ request, locals }) => {
|
createKey: async ({ request, locals }) => {
|
||||||
if (!locals.user) throw error(401, 'Unauthorized');
|
if (!isStaff(locals)) throw error(403, 'Not authorized');
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const email = formData.get('email')?.toString()?.trim().toLowerCase();
|
const email = formData.get('email')?.toString()?.trim().toLowerCase();
|
||||||
@@ -60,7 +61,7 @@ export const actions = {
|
|||||||
return { success: true, action: 'createKey' };
|
return { success: true, action: 'createKey' };
|
||||||
},
|
},
|
||||||
replace: async ({ request, locals }) => {
|
replace: async ({ request, locals }) => {
|
||||||
if (!locals.user) throw error(401, 'Unauthorized');
|
if (!isStaff(locals)) throw error(403, 'Not authorized');
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const keyId = formData.get('id')?.toString();
|
const keyId = formData.get('id')?.toString();
|
||||||
|
|||||||
@@ -48,11 +48,11 @@ describe('admin license-keys createKey action', () => {
|
|||||||
const req = createFormRequest({ email: 'jane@example.com' });
|
const req = createFormRequest({ email: 'jane@example.com' });
|
||||||
|
|
||||||
const err = await actions.createKey({ request: req, locals } as any).catch((e) => e);
|
const err = await actions.createKey({ request: req, locals } as any).catch((e) => e);
|
||||||
expect(isHttpError(err, 401)).toBe(true);
|
expect(isHttpError(err, 403)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns error when email is missing', async () => {
|
it('returns error when email is missing', async () => {
|
||||||
const locals = createLocals(mockUser() as any);
|
const locals = createLocals(mockUser({ roleId: 3 }) as any);
|
||||||
const req = createFormRequest({});
|
const req = createFormRequest({});
|
||||||
|
|
||||||
const result = await actions.createKey({ request: req, locals } as any);
|
const result = await actions.createKey({ request: req, locals } as any);
|
||||||
@@ -65,7 +65,7 @@ describe('admin license-keys createKey action', () => {
|
|||||||
mockLicenseKeyCreate.mockResolvedValue({});
|
mockLicenseKeyCreate.mockResolvedValue({});
|
||||||
sendMail.mockResolvedValue({ id: 'email-1' });
|
sendMail.mockResolvedValue({ id: 'email-1' });
|
||||||
|
|
||||||
const locals = createLocals(mockUser() as any);
|
const locals = createLocals(mockUser({ roleId: 3 }) as any);
|
||||||
const req = createFormRequest({ email: 'nobody@example.com' });
|
const req = createFormRequest({ email: 'nobody@example.com' });
|
||||||
|
|
||||||
const result = await actions.createKey({ request: req, locals } as any);
|
const result = await actions.createKey({ request: req, locals } as any);
|
||||||
@@ -86,7 +86,7 @@ describe('admin license-keys createKey action', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('creates a license key and sends email with license_key variable', async () => {
|
it('creates a license key and sends email with license_key variable', async () => {
|
||||||
const user = mockUser();
|
const user = mockUser({ roleId: 3 });
|
||||||
const profile = { firstName: 'Jane' };
|
const profile = { firstName: 'Jane' };
|
||||||
|
|
||||||
mockFindUnique.mockResolvedValue(user);
|
mockFindUnique.mockResolvedValue(user);
|
||||||
@@ -117,7 +117,7 @@ describe('admin license-keys createKey action', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('sends email with license_key even when profile is missing', async () => {
|
it('sends email with license_key even when profile is missing', async () => {
|
||||||
const user = mockUser();
|
const user = mockUser({ roleId: 3 });
|
||||||
|
|
||||||
mockFindUnique.mockResolvedValue(user);
|
mockFindUnique.mockResolvedValue(user);
|
||||||
mockProfileFindFirst.mockResolvedValue(null);
|
mockProfileFindFirst.mockResolvedValue(null);
|
||||||
@@ -136,7 +136,7 @@ describe('admin license-keys createKey action', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('still returns success when email sending fails', async () => {
|
it('still returns success when email sending fails', async () => {
|
||||||
const user = mockUser();
|
const user = mockUser({ roleId: 3 });
|
||||||
|
|
||||||
mockFindUnique.mockResolvedValue(user);
|
mockFindUnique.mockResolvedValue(user);
|
||||||
mockProfileFindFirst.mockResolvedValue({ firstName: 'Jane' });
|
mockProfileFindFirst.mockResolvedValue({ firstName: 'Jane' });
|
||||||
@@ -166,11 +166,11 @@ describe('admin license-keys replace action', () => {
|
|||||||
const req = createFormRequest({ id: 'k1' });
|
const req = createFormRequest({ id: 'k1' });
|
||||||
|
|
||||||
const err = await actions.replace({ request: req, locals } as any).catch((e) => e);
|
const err = await actions.replace({ request: req, locals } as any).catch((e) => e);
|
||||||
expect(isHttpError(err, 401)).toBe(true);
|
expect(isHttpError(err, 403)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns error when id is missing', async () => {
|
it('returns error when id is missing', async () => {
|
||||||
const locals = createLocals(mockUser() as any);
|
const locals = createLocals(mockUser({ roleId: 3 }) as any);
|
||||||
const req = createFormRequest({});
|
const req = createFormRequest({});
|
||||||
|
|
||||||
const result = await actions.replace({ request: req, locals } as any);
|
const result = await actions.replace({ request: req, locals } as any);
|
||||||
@@ -180,7 +180,7 @@ describe('admin license-keys replace action', () => {
|
|||||||
it('returns error when the key does not exist', async () => {
|
it('returns error when the key does not exist', async () => {
|
||||||
mockLicenseKeyFindUnique.mockResolvedValue(null);
|
mockLicenseKeyFindUnique.mockResolvedValue(null);
|
||||||
|
|
||||||
const locals = createLocals(mockUser() as any);
|
const locals = createLocals(mockUser({ roleId: 3 }) as any);
|
||||||
const req = createFormRequest({ id: 'missing' });
|
const req = createFormRequest({ id: 'missing' });
|
||||||
|
|
||||||
const result = await actions.replace({ request: req, locals } as any);
|
const result = await actions.replace({ request: req, locals } as any);
|
||||||
@@ -197,7 +197,7 @@ describe('admin license-keys replace action', () => {
|
|||||||
mockLicenseKeyDelete.mockResolvedValue({});
|
mockLicenseKeyDelete.mockResolvedValue({});
|
||||||
mockLicenseKeyCreate.mockResolvedValue({});
|
mockLicenseKeyCreate.mockResolvedValue({});
|
||||||
|
|
||||||
const locals = createLocals(mockUser() as any);
|
const locals = createLocals(mockUser({ roleId: 3 }) as any);
|
||||||
const req = createFormRequest({ id: 'k1' });
|
const req = createFormRequest({ id: 'k1' });
|
||||||
|
|
||||||
const result = await actions.replace({ request: req, locals } as any);
|
const result = await actions.replace({ request: req, locals } as any);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import { prisma } from '$lib/db';
|
import { prisma } from '$lib/db';
|
||||||
|
import { isAdmin, isStaff } from '$lib/server/roles';
|
||||||
import { superValidate } from 'sveltekit-superforms/server';
|
import { superValidate } from 'sveltekit-superforms/server';
|
||||||
import { zod4 } from 'sveltekit-superforms/adapters';
|
import { zod4 } from 'sveltekit-superforms/adapters';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -17,7 +18,11 @@ export const load: PageServerLoad = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {
|
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 formData = await request.formData();
|
||||||
const form = await superValidate(formData, zod4(CategorySchema));
|
const form = await superValidate(formData, zod4(CategorySchema));
|
||||||
if (!form.valid) {
|
if (!form.valid) {
|
||||||
@@ -58,7 +63,11 @@ export const actions = {
|
|||||||
|
|
||||||
return { success: true, action: 'upsert' };
|
return { success: true, action: 'upsert' };
|
||||||
},
|
},
|
||||||
delete: async ({ request }) => {
|
delete: async ({ request, locals }) => {
|
||||||
|
if (!isAdmin(locals)) {
|
||||||
|
return { success: false, error: 'Not authorized' };
|
||||||
|
}
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const rawId = formData.get('id')?.toString();
|
const rawId = formData.get('id')?.toString();
|
||||||
if (!rawId) return { success: false, error: 'Missing id' };
|
if (!rawId) return { success: false, error: 'Missing id' };
|
||||||
@@ -77,7 +86,11 @@ export const actions = {
|
|||||||
|
|
||||||
return { success: true, action: 'delete' };
|
return { success: true, action: 'delete' };
|
||||||
},
|
},
|
||||||
reorder: async ({ request }) => {
|
reorder: async ({ request, locals }) => {
|
||||||
|
if (!isStaff(locals)) {
|
||||||
|
return { success: false, error: 'Not authorized' };
|
||||||
|
}
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const raw = formData.get('ids')?.toString();
|
const raw = formData.get('ids')?.toString();
|
||||||
if (!raw) return { success: false, error: 'Missing ids' };
|
if (!raw) return { success: false, error: 'Missing ids' };
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import { prisma } from '$lib/db';
|
import { prisma } from '$lib/db';
|
||||||
|
import { isAdmin, isStaff } from '$lib/server/roles';
|
||||||
import { superValidate } from 'sveltekit-superforms/server';
|
import { superValidate } from 'sveltekit-superforms/server';
|
||||||
import { zod4 } from 'sveltekit-superforms/adapters';
|
import { zod4 } from 'sveltekit-superforms/adapters';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -17,7 +18,11 @@ export const load: PageServerLoad = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {
|
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 formData = await request.formData();
|
||||||
const form = await superValidate(formData, zod4(GroupSchema));
|
const form = await superValidate(formData, zod4(GroupSchema));
|
||||||
if (!form.valid) {
|
if (!form.valid) {
|
||||||
@@ -58,7 +63,11 @@ export const actions = {
|
|||||||
|
|
||||||
return { success: true, action: 'upsert' };
|
return { success: true, action: 'upsert' };
|
||||||
},
|
},
|
||||||
delete: async ({ request }) => {
|
delete: async ({ request, locals }) => {
|
||||||
|
if (!isAdmin(locals)) {
|
||||||
|
return { success: false, error: 'Not authorized' };
|
||||||
|
}
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const rawId = formData.get('id')?.toString();
|
const rawId = formData.get('id')?.toString();
|
||||||
if (!rawId) return { success: false, error: 'Missing id' };
|
if (!rawId) return { success: false, error: 'Missing id' };
|
||||||
@@ -77,7 +86,11 @@ export const actions = {
|
|||||||
|
|
||||||
return { success: true, action: 'delete' };
|
return { success: true, action: 'delete' };
|
||||||
},
|
},
|
||||||
reorder: async ({ request }) => {
|
reorder: async ({ request, locals }) => {
|
||||||
|
if (!isStaff(locals)) {
|
||||||
|
return { success: false, error: 'Not authorized' };
|
||||||
|
}
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const raw = formData.get('ids')?.toString();
|
const raw = formData.get('ids')?.toString();
|
||||||
if (!raw) return { success: false, error: 'Missing ids' };
|
if (!raw) return { success: false, error: 'Missing ids' };
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import { prisma } from '$lib/db';
|
import { prisma } from '$lib/db';
|
||||||
|
import { isSuper } from '$lib/server/roles';
|
||||||
import { superValidate } from 'sveltekit-superforms/server';
|
import { superValidate } from 'sveltekit-superforms/server';
|
||||||
import { zod4 } from 'sveltekit-superforms/adapters';
|
import { zod4 } from 'sveltekit-superforms/adapters';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -13,18 +14,6 @@ const ProclamationSchema = z.object({
|
|||||||
content: z.string().optional().default('')
|
content: z.string().optional().default('')
|
||||||
});
|
});
|
||||||
|
|
||||||
async function isSuper(locals: App.Locals): Promise<boolean> {
|
|
||||||
if (!locals.user?.id) return false;
|
|
||||||
|
|
||||||
// The Better Auth session user does not include `roleId`, so read the
|
|
||||||
// current role from the database.
|
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: { id: locals.user.id },
|
|
||||||
select: { roleId: true }
|
|
||||||
});
|
|
||||||
return (user?.roleId ?? 4) <= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ locals }) => {
|
export const load: PageServerLoad = async ({ locals }) => {
|
||||||
const [proclamations, frequencies, categories, groups] = await Promise.all([
|
const [proclamations, frequencies, categories, groups] = await Promise.all([
|
||||||
prisma.proclamation.findMany({
|
prisma.proclamation.findMany({
|
||||||
@@ -48,7 +37,7 @@ export const load: PageServerLoad = async ({ locals }) => {
|
|||||||
const records = proclamations as unknown as Record<string, unknown>[];
|
const records = proclamations as unknown as Record<string, unknown>[];
|
||||||
|
|
||||||
// Strip content from non-Super users
|
// Strip content from non-Super users
|
||||||
if (!(await isSuper(locals))) {
|
if (!isSuper(locals)) {
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
delete record.content;
|
delete record.content;
|
||||||
}
|
}
|
||||||
@@ -63,7 +52,7 @@ export const actions = {
|
|||||||
return { success: false, action: 'upsert', error: 'Unauthorized' };
|
return { success: false, action: 'upsert', error: 'Unauthorized' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await isSuper(locals))) {
|
if (!isSuper(locals)) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
action: 'upsert',
|
action: 'upsert',
|
||||||
@@ -106,7 +95,7 @@ export const actions = {
|
|||||||
return { success: true, action: 'upsert' };
|
return { success: true, action: 'upsert' };
|
||||||
},
|
},
|
||||||
delete: async ({ request, locals }) => {
|
delete: async ({ request, locals }) => {
|
||||||
if (!(await isSuper(locals))) {
|
if (!isSuper(locals)) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
action: 'delete',
|
action: 'delete',
|
||||||
|
|||||||
@@ -18,9 +18,6 @@ vi.mock('$lib/db', () => ({
|
|||||||
},
|
},
|
||||||
proclamationGroup: {
|
proclamationGroup: {
|
||||||
findMany: vi.fn()
|
findMany: vi.fn()
|
||||||
},
|
|
||||||
user: {
|
|
||||||
findUnique: vi.fn()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@@ -36,7 +33,6 @@ const mockProclamationDelete = prisma.proclamation.delete as ReturnType<typeof v
|
|||||||
const mockFreqFindMany = prisma.solfeggioFrequency.findMany as ReturnType<typeof vi.fn>;
|
const mockFreqFindMany = prisma.solfeggioFrequency.findMany as ReturnType<typeof vi.fn>;
|
||||||
const mockCatFindMany = prisma.proclamationCategory.findMany as ReturnType<typeof vi.fn>;
|
const mockCatFindMany = prisma.proclamationCategory.findMany as ReturnType<typeof vi.fn>;
|
||||||
const mockGroupFindMany = prisma.proclamationGroup.findMany as ReturnType<typeof vi.fn>;
|
const mockGroupFindMany = prisma.proclamationGroup.findMany as ReturnType<typeof vi.fn>;
|
||||||
const mockUserFindUnique = prisma.user.findUnique as ReturnType<typeof vi.fn>;
|
|
||||||
|
|
||||||
const frequencies = [{ id: 1, name: 'UT', value: 396 }];
|
const frequencies = [{ id: 1, name: 'UT', value: 396 }];
|
||||||
const categories = [{ id: 1, name: 'Healing' }];
|
const categories = [{ id: 1, name: 'Healing' }];
|
||||||
@@ -56,8 +52,6 @@ function validRequest(overrides: Record<string, string> = {}) {
|
|||||||
describe('proclamations admin page server', () => {
|
describe('proclamations admin page server', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
// Default the caller to SUPER (1); override per test as needed.
|
|
||||||
mockUserFindUnique.mockResolvedValue({ roleId: 1 });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('load', () => {
|
describe('load', () => {
|
||||||
@@ -74,7 +68,6 @@ describe('proclamations admin page server', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('strips content for non-Super users', async () => {
|
it('strips content for non-Super users', async () => {
|
||||||
mockUserFindUnique.mockResolvedValue({ roleId: 4 });
|
|
||||||
const locals = createLocals(mockUser({ roleId: 4 }) as any);
|
const locals = createLocals(mockUser({ roleId: 4 }) as any);
|
||||||
const result = await load({ locals } as any);
|
const result = await load({ locals } as any);
|
||||||
|
|
||||||
@@ -116,7 +109,6 @@ describe('proclamations admin page server', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('rejects non-Super users', async () => {
|
it('rejects non-Super users', async () => {
|
||||||
mockUserFindUnique.mockResolvedValue({ roleId: 4 });
|
|
||||||
const locals = createLocals(mockUser({ roleId: 4 }) as any);
|
const locals = createLocals(mockUser({ roleId: 4 }) as any);
|
||||||
const req = createFormRequest({ name: 'test' });
|
const req = createFormRequest({ name: 'test' });
|
||||||
|
|
||||||
@@ -157,21 +149,6 @@ describe('proclamations admin page server', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows a Super user whose session user has no roleId', async () => {
|
|
||||||
mockProclamationCreate.mockResolvedValue({ id: 'p1' });
|
|
||||||
|
|
||||||
// The real Better Auth session user does not carry `roleId`; the
|
|
||||||
// action must look the role up in the database instead.
|
|
||||||
const locals = createLocals(mockUser({ roleId: null }) as any);
|
|
||||||
const result = await actions.upsert({ request: validRequest(), locals } as any);
|
|
||||||
|
|
||||||
expect(result).toEqual({ success: true, action: 'upsert' });
|
|
||||||
expect(mockUserFindUnique).toHaveBeenCalledWith({
|
|
||||||
where: { id: 'user-1' },
|
|
||||||
select: { roleId: true }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updates an existing proclamation for a Super user', async () => {
|
it('updates an existing proclamation for a Super user', async () => {
|
||||||
mockProclamationFindUnique.mockResolvedValue(mockProclamation());
|
mockProclamationFindUnique.mockResolvedValue(mockProclamation());
|
||||||
mockProclamationUpdate.mockResolvedValue({});
|
mockProclamationUpdate.mockResolvedValue({});
|
||||||
@@ -223,7 +200,6 @@ describe('proclamations admin page server', () => {
|
|||||||
|
|
||||||
describe('delete action', () => {
|
describe('delete action', () => {
|
||||||
it('rejects non-Super users', async () => {
|
it('rejects non-Super users', async () => {
|
||||||
mockUserFindUnique.mockResolvedValue({ roleId: 4 });
|
|
||||||
const locals = createLocals(mockUser({ roleId: 4 }) as any);
|
const locals = createLocals(mockUser({ roleId: 4 }) as any);
|
||||||
const req = createFormRequest({ id: '1' });
|
const req = createFormRequest({ id: '1' });
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import { prisma } from '$lib/db';
|
import { prisma } from '$lib/db';
|
||||||
|
import { isAdmin, isStaff } from '$lib/server/roles';
|
||||||
import { superValidate } from 'sveltekit-superforms/server';
|
import { superValidate } from 'sveltekit-superforms/server';
|
||||||
import { zod4 } from 'sveltekit-superforms/adapters';
|
import { zod4 } from 'sveltekit-superforms/adapters';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -45,7 +46,11 @@ export const load: PageServerLoad = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {
|
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 formData = await request.formData();
|
||||||
const form = await superValidate(formData, zod4(ProfileSchema));
|
const form = await superValidate(formData, zod4(ProfileSchema));
|
||||||
if (!form.valid) {
|
if (!form.valid) {
|
||||||
@@ -112,14 +117,22 @@ export const actions = {
|
|||||||
}
|
}
|
||||||
return { success: true, action: 'upsert' };
|
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 formData = await request.formData();
|
||||||
const rawId = formData.get('id')?.toString();
|
const rawId = formData.get('id')?.toString();
|
||||||
if (!rawId) return { success: false, error: 'Missing id' };
|
if (!rawId) return { success: false, error: 'Missing id' };
|
||||||
await prisma.profile.delete({ where: { id: rawId } });
|
await prisma.profile.delete({ where: { id: rawId } });
|
||||||
return { success: true, action: 'delete' };
|
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 formData = await request.formData();
|
||||||
const form = await superValidate(formData, zod4(AddressSchema));
|
const form = await superValidate(formData, zod4(AddressSchema));
|
||||||
if (!form.valid) {
|
if (!form.valid) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
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', () => ({
|
vi.mock('$lib/db', () => ({
|
||||||
prisma: {
|
prisma: {
|
||||||
@@ -41,6 +41,9 @@ function makeProfileRequest(overrides: Record<string, string> = {}) {
|
|||||||
return createFormRequest({ ...validFields, ...overrides });
|
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', () => {
|
describe('profiles admin page server', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@@ -73,7 +76,10 @@ describe('profiles admin page server', () => {
|
|||||||
mockUserFindUnique.mockResolvedValue({ id: 'user-1', email: validFields.email });
|
mockUserFindUnique.mockResolvedValue({ id: 'user-1', email: validFields.email });
|
||||||
mockProfileCreate.mockResolvedValue({ id: 'p-new' });
|
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(result).toEqual({ success: true, action: 'upsert' });
|
||||||
expect(mockUserFindUnique).toHaveBeenCalledWith({ where: { email: validFields.email } });
|
expect(mockUserFindUnique).toHaveBeenCalledWith({ where: { email: validFields.email } });
|
||||||
@@ -86,7 +92,10 @@ describe('profiles admin page server', () => {
|
|||||||
mockUserFindUnique.mockResolvedValue(null);
|
mockUserFindUnique.mockResolvedValue(null);
|
||||||
mockProfileCreate.mockResolvedValue({ id: 'p-new' });
|
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(result).toEqual({ success: true, action: 'upsert' });
|
||||||
expect(mockProfileCreate).toHaveBeenCalledWith({ data: validFields });
|
expect(mockProfileCreate).toHaveBeenCalledWith({ data: validFields });
|
||||||
@@ -96,7 +105,10 @@ describe('profiles admin page server', () => {
|
|||||||
mockUserFindUnique.mockResolvedValue(null);
|
mockUserFindUnique.mockResolvedValue(null);
|
||||||
mockProfileCreate.mockRejectedValue({ code: 'P2002' });
|
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({
|
expect(result).toEqual({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -119,7 +131,7 @@ describe('profiles admin page server', () => {
|
|||||||
mockProfileUpdate.mockResolvedValue({});
|
mockProfileUpdate.mockResolvedValue({});
|
||||||
|
|
||||||
const request = makeProfileRequest({ email: 'new@example.com', id: 'p1' });
|
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(result).toEqual({ success: true, action: 'upsert' });
|
||||||
expect(mockUserUpdate).toHaveBeenCalledWith({
|
expect(mockUserUpdate).toHaveBeenCalledWith({
|
||||||
@@ -142,7 +154,7 @@ describe('profiles admin page server', () => {
|
|||||||
mockProfileUpdate.mockResolvedValue({});
|
mockProfileUpdate.mockResolvedValue({});
|
||||||
|
|
||||||
const request = makeProfileRequest({ id: 'p1' });
|
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(result).toEqual({ success: true, action: 'upsert' });
|
||||||
expect(mockUserFindFirst).not.toHaveBeenCalled();
|
expect(mockUserFindFirst).not.toHaveBeenCalled();
|
||||||
@@ -159,7 +171,7 @@ describe('profiles admin page server', () => {
|
|||||||
mockUserFindFirst.mockResolvedValue({ id: 'user-2' });
|
mockUserFindFirst.mockResolvedValue({ id: 'user-2' });
|
||||||
|
|
||||||
const request = makeProfileRequest({ email: 'taken@example.com', id: 'p1' });
|
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({
|
expect(result).toEqual({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -181,7 +193,7 @@ describe('profiles admin page server', () => {
|
|||||||
mockProfileUpdate.mockResolvedValue({});
|
mockProfileUpdate.mockResolvedValue({});
|
||||||
|
|
||||||
const request = makeProfileRequest({ id: 'p1' });
|
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(result).toEqual({ success: true, action: 'upsert' });
|
||||||
expect(mockProfileUpdate).toHaveBeenCalledWith({
|
expect(mockProfileUpdate).toHaveBeenCalledWith({
|
||||||
@@ -194,7 +206,7 @@ describe('profiles admin page server', () => {
|
|||||||
describe('upsert - validation', () => {
|
describe('upsert - validation', () => {
|
||||||
it('returns field errors for invalid input', async () => {
|
it('returns field errors for invalid input', async () => {
|
||||||
const request = makeProfileRequest({ email: 'not-an-email' });
|
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.success).toBe(false);
|
||||||
expect(result.action).toBe('upsert');
|
expect(result.action).toBe('upsert');
|
||||||
@@ -205,7 +217,7 @@ describe('profiles admin page server', () => {
|
|||||||
mockProfileFindUnique.mockResolvedValue(null);
|
mockProfileFindUnique.mockResolvedValue(null);
|
||||||
|
|
||||||
const request = makeProfileRequest({ id: 'missing' });
|
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' });
|
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 () => {
|
it('deletes the profile by id', async () => {
|
||||||
mockProfileDelete.mockResolvedValue({});
|
mockProfileDelete.mockResolvedValue({});
|
||||||
|
|
||||||
const locals = createLocals();
|
const locals = admin();
|
||||||
const req = createFormRequest({ id: 'p1' });
|
const req = createFormRequest({ id: 'p1' });
|
||||||
const result = await actions.delete({ request: req, locals } as any);
|
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 () => {
|
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);
|
const result = await actions.delete({ request: createFormRequest({}), locals } as any);
|
||||||
|
|
||||||
expect(result).toEqual({ success: false, error: 'Missing id' });
|
expect(result).toEqual({ success: false, error: 'Missing id' });
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { createLocals, createFormRequest, mockUser } from '$lib/test-utils';
|
||||||
|
|
||||||
|
vi.mock('$lib/db', () => ({
|
||||||
|
prisma: {
|
||||||
|
userRole: { create: vi.fn(), update: vi.fn(), delete: vi.fn() },
|
||||||
|
solfeggioFrequency: { create: vi.fn(), update: vi.fn(), delete: vi.fn() },
|
||||||
|
proclamationCategory: { findFirst: vi.fn(), create: vi.fn(), update: vi.fn(), delete: vi.fn() },
|
||||||
|
proclamationGroup: { findFirst: vi.fn(), create: vi.fn(), update: vi.fn(), delete: vi.fn() }
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { prisma } from '$lib/db';
|
||||||
|
import { actions as userRoles } from './user-roles/+page.server';
|
||||||
|
import { actions as frequencies } from './solfeggio-frequencies/+page.server';
|
||||||
|
import { actions as categories } from './proclamation-categories/+page.server';
|
||||||
|
import { actions as groups } from './proclamation-groups/+page.server';
|
||||||
|
|
||||||
|
const staff = () => createLocals(mockUser({ roleId: 3 }) as any);
|
||||||
|
const admin = () => createLocals(mockUser({ roleId: 2 }) as any);
|
||||||
|
const user = () => createLocals(mockUser({ roleId: 4 }) as any);
|
||||||
|
const anonymous = () => createLocals();
|
||||||
|
|
||||||
|
type CrudActions = {
|
||||||
|
upsert: (event: unknown) => Promise<unknown>;
|
||||||
|
delete: (event: unknown) => Promise<unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pages: Array<[string, CrudActions]> = [
|
||||||
|
['user-roles', userRoles as unknown as CrudActions],
|
||||||
|
['solfeggio-frequencies', frequencies as unknown as CrudActions],
|
||||||
|
['proclamation-categories', categories as unknown as CrudActions],
|
||||||
|
['proclamation-groups', groups as unknown as CrudActions]
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('admin CRUD action role guards', () => {
|
||||||
|
for (const [name, actions] of pages) {
|
||||||
|
it(`${name}: upsert rejects an anonymous request`, async () => {
|
||||||
|
const result = await actions.upsert({
|
||||||
|
request: createFormRequest({ name: 'x' }),
|
||||||
|
locals: anonymous()
|
||||||
|
});
|
||||||
|
expect(result).toEqual({ success: false, action: 'upsert', error: 'Not authorized' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`${name}: upsert rejects a regular user`, async () => {
|
||||||
|
const result = await actions.upsert({
|
||||||
|
request: createFormRequest({ name: 'x' }),
|
||||||
|
locals: user()
|
||||||
|
});
|
||||||
|
expect(result).toEqual({ success: false, action: 'upsert', error: 'Not authorized' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`${name}: delete rejects a staff (non-admin) user`, async () => {
|
||||||
|
const result = await actions.delete({
|
||||||
|
request: createFormRequest({ id: '1' }),
|
||||||
|
locals: staff()
|
||||||
|
});
|
||||||
|
expect(result).toEqual({ success: false, error: 'Not authorized' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('admin CRUD action role guards: authorized users still work', () => {
|
||||||
|
it('staff can create a user role', async () => {
|
||||||
|
(prisma.userRole.create as ReturnType<typeof vi.fn>).mockResolvedValue({ id: 5 });
|
||||||
|
const result = await userRoles.upsert({
|
||||||
|
request: createFormRequest({ name: 'New Role' }),
|
||||||
|
locals: staff()
|
||||||
|
} as any);
|
||||||
|
expect(result).toEqual({ success: true, action: 'upsert' });
|
||||||
|
expect(prisma.userRole.create).toHaveBeenCalledWith({ data: { name: 'New Role' } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('admin can delete a user role', async () => {
|
||||||
|
(prisma.userRole.delete as ReturnType<typeof vi.fn>).mockResolvedValue({});
|
||||||
|
const result = await userRoles.delete({
|
||||||
|
request: createFormRequest({ id: '5' }),
|
||||||
|
locals: admin()
|
||||||
|
} as any);
|
||||||
|
expect(result).toEqual({ success: true, action: 'delete' });
|
||||||
|
expect(prisma.userRole.delete).toHaveBeenCalledWith({ where: { id: 5 } });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import { prisma } from '$lib/db';
|
import { prisma } from '$lib/db';
|
||||||
|
import { isAdmin, isStaff } from '$lib/server/roles';
|
||||||
import { superValidate } from 'sveltekit-superforms/server';
|
import { superValidate } from 'sveltekit-superforms/server';
|
||||||
import { zod4 } from 'sveltekit-superforms/adapters';
|
import { zod4 } from 'sveltekit-superforms/adapters';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -16,7 +17,11 @@ export const load: PageServerLoad = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {
|
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 formData = await request.formData();
|
||||||
const form = await superValidate(formData, zod4(SolfeggioSchema));
|
const form = await superValidate(formData, zod4(SolfeggioSchema));
|
||||||
if (!form.valid) {
|
if (!form.valid) {
|
||||||
@@ -48,7 +53,11 @@ export const actions = {
|
|||||||
|
|
||||||
return { success: true, action: 'upsert' };
|
return { success: true, action: 'upsert' };
|
||||||
},
|
},
|
||||||
delete: async ({ request }) => {
|
delete: async ({ request, locals }) => {
|
||||||
|
if (!isAdmin(locals)) {
|
||||||
|
return { success: false, error: 'Not authorized' };
|
||||||
|
}
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const rawId = formData.get('id')?.toString();
|
const rawId = formData.get('id')?.toString();
|
||||||
if (!rawId) return { success: false, error: 'Missing id' };
|
if (!rawId) return { success: false, error: 'Missing id' };
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import { prisma } from '$lib/db';
|
import { prisma } from '$lib/db';
|
||||||
|
import { isAdmin, isStaff } from '$lib/server/roles';
|
||||||
import { superValidate } from 'sveltekit-superforms/server';
|
import { superValidate } from 'sveltekit-superforms/server';
|
||||||
import { zod4 } from 'sveltekit-superforms/adapters';
|
import { zod4 } from 'sveltekit-superforms/adapters';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -15,7 +16,11 @@ export const load: PageServerLoad = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {
|
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 formData = await request.formData();
|
||||||
const form = await superValidate(formData, zod4(UserRoleSchema));
|
const form = await superValidate(formData, zod4(UserRoleSchema));
|
||||||
if (!form.valid) {
|
if (!form.valid) {
|
||||||
@@ -43,7 +48,11 @@ export const actions = {
|
|||||||
|
|
||||||
return { success: true, action: 'upsert' };
|
return { success: true, action: 'upsert' };
|
||||||
},
|
},
|
||||||
delete: async ({ request }) => {
|
delete: async ({ request, locals }) => {
|
||||||
|
if (!isAdmin(locals)) {
|
||||||
|
return { success: false, error: 'Not authorized' };
|
||||||
|
}
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const rawId = formData.get('id')?.toString();
|
const rawId = formData.get('id')?.toString();
|
||||||
if (!rawId) return { success: false, error: 'Missing id' };
|
if (!rawId) return { success: false, error: 'Missing id' };
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { PageServerLoad, Actions } from './$types';
|
import type { PageServerLoad, Actions } from './$types';
|
||||||
import { prisma } from '$lib/db';
|
import { prisma } from '$lib/db';
|
||||||
import { sendRegistrationInvite } from '$lib/server/registration-invite';
|
import { sendRegistrationInvite } from '$lib/server/registration-invite';
|
||||||
|
import { isAdmin, isStaff, roleId as viewerRoleId } from '$lib/server/roles';
|
||||||
import { superValidate } from 'sveltekit-superforms/server';
|
import { superValidate } from 'sveltekit-superforms/server';
|
||||||
import { zod4 } from 'sveltekit-superforms/adapters';
|
import { zod4 } from 'sveltekit-superforms/adapters';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -12,16 +13,8 @@ const UserSchema = z.object({
|
|||||||
roleId: z.coerce.number().int().min(1)
|
roleId: z.coerce.number().int().min(1)
|
||||||
});
|
});
|
||||||
|
|
||||||
async function myRoleId(locals: App.Locals): Promise<number> {
|
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: { id: locals.user?.id },
|
|
||||||
select: { roleId: true }
|
|
||||||
});
|
|
||||||
return user?.roleId ?? 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ locals }) => {
|
export const load: PageServerLoad = async ({ locals }) => {
|
||||||
const roleId = await myRoleId(locals);
|
const myRole = viewerRoleId(locals);
|
||||||
|
|
||||||
const [users, roles] = await Promise.all([
|
const [users, roles] = await Promise.all([
|
||||||
prisma.user.findMany({
|
prisma.user.findMany({
|
||||||
@@ -31,7 +24,7 @@ export const load: PageServerLoad = async ({ locals }) => {
|
|||||||
prisma.userRole.findMany({ orderBy: { id: 'asc' } })
|
prisma.userRole.findMany({ orderBy: { id: 'asc' } })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const roleOptions = roles.filter((role) => role.id >= roleId);
|
const roleOptions = roles.filter((role) => role.id >= myRole);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
records: users as unknown as Record<string, unknown>[],
|
records: users as unknown as Record<string, unknown>[],
|
||||||
@@ -41,6 +34,10 @@ export const load: PageServerLoad = async ({ locals }) => {
|
|||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
upsert: async ({ request, locals }) => {
|
upsert: async ({ request, locals }) => {
|
||||||
|
if (!isStaff(locals)) {
|
||||||
|
return { success: false, action: 'upsert', error: 'Not authorized' };
|
||||||
|
}
|
||||||
|
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const form = await superValidate(formData, zod4(UserSchema));
|
const form = await superValidate(formData, zod4(UserSchema));
|
||||||
if (!form.valid) {
|
if (!form.valid) {
|
||||||
@@ -57,7 +54,7 @@ export const actions: Actions = {
|
|||||||
roleId: form.data.roleId
|
roleId: form.data.roleId
|
||||||
};
|
};
|
||||||
|
|
||||||
const myRole = await myRoleId(locals);
|
const myRole = viewerRoleId(locals);
|
||||||
if (myRole > 1 && data.roleId < myRole) {
|
if (myRole > 1 && data.roleId < myRole) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -104,7 +101,11 @@ export const actions: Actions = {
|
|||||||
|
|
||||||
return { success: true, action: 'upsert' };
|
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 formData = await request.formData();
|
||||||
const rawId = formData.get('id')?.toString();
|
const rawId = formData.get('id')?.toString();
|
||||||
if (!rawId) return { success: false, error: 'Missing id' };
|
if (!rawId) return { success: false, error: 'Missing id' };
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ vi.mock('$lib/server/registration-invite', () => ({
|
|||||||
import { prisma } from '$lib/db';
|
import { prisma } from '$lib/db';
|
||||||
import { actions } from './+page.server';
|
import { actions } from './+page.server';
|
||||||
|
|
||||||
const userFindUnique = prisma.user.findUnique as ReturnType<typeof vi.fn>;
|
|
||||||
const userCreate = prisma.user.create as ReturnType<typeof vi.fn>;
|
const userCreate = prisma.user.create as ReturnType<typeof vi.fn>;
|
||||||
const userUpdate = prisma.user.update as ReturnType<typeof vi.fn>;
|
const userUpdate = prisma.user.update as ReturnType<typeof vi.fn>;
|
||||||
|
|
||||||
@@ -44,14 +43,13 @@ function upsertRequest(overrides: Record<string, string> = {}) {
|
|||||||
describe('users admin page server upsert', () => {
|
describe('users admin page server upsert', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
userFindUnique.mockResolvedValue({ roleId: 1 });
|
|
||||||
userCreate.mockResolvedValue({ id: 'new-user' });
|
userCreate.mockResolvedValue({ id: 'new-user' });
|
||||||
userUpdate.mockResolvedValue({ id: 'user-1' });
|
userUpdate.mockResolvedValue({ id: 'user-1' });
|
||||||
sendRegistrationInvite.mockResolvedValue(undefined);
|
sendRegistrationInvite.mockResolvedValue(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('marks a created user verified and emails a set-password link', async () => {
|
it('marks a created user verified and emails a set-password link', async () => {
|
||||||
const locals = createLocals(mockUser() as any);
|
const locals = createLocals(mockUser({ roleId: 1 }) as any);
|
||||||
const result = await invokeUpsert(upsertRequest(), locals);
|
const result = await invokeUpsert(upsertRequest(), locals);
|
||||||
|
|
||||||
expect(result).toEqual({ success: true, action: 'upsert' });
|
expect(result).toEqual({ success: true, action: 'upsert' });
|
||||||
@@ -66,7 +64,7 @@ describe('users admin page server upsert', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('does not email a registration link when updating an existing user', async () => {
|
it('does not email a registration link when updating an existing user', async () => {
|
||||||
const locals = createLocals(mockUser() as any);
|
const locals = createLocals(mockUser({ roleId: 1 }) as any);
|
||||||
const result = await invokeUpsert(upsertRequest({ id: 'user-9' }), locals);
|
const result = await invokeUpsert(upsertRequest({ id: 'user-9' }), locals);
|
||||||
|
|
||||||
expect(result).toEqual({ success: true, action: 'upsert' });
|
expect(result).toEqual({ success: true, action: 'upsert' });
|
||||||
@@ -78,7 +76,7 @@ describe('users admin page server upsert', () => {
|
|||||||
it('still succeeds when the registration email fails to send', async () => {
|
it('still succeeds when the registration email fails to send', async () => {
|
||||||
sendRegistrationInvite.mockRejectedValue(new Error('resend down'));
|
sendRegistrationInvite.mockRejectedValue(new Error('resend down'));
|
||||||
|
|
||||||
const locals = createLocals(mockUser() as any);
|
const locals = createLocals(mockUser({ roleId: 1 }) as any);
|
||||||
const result = await invokeUpsert(upsertRequest(), locals);
|
const result = await invokeUpsert(upsertRequest(), locals);
|
||||||
|
|
||||||
expect(result).toEqual({ success: true, action: 'upsert' });
|
expect(result).toEqual({ success: true, action: 'upsert' });
|
||||||
@@ -86,9 +84,7 @@ describe('users admin page server upsert', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('blocks assigning a role with more privileges than the actor', async () => {
|
it('blocks assigning a role with more privileges than the actor', async () => {
|
||||||
userFindUnique.mockResolvedValue({ roleId: 2 });
|
const locals = createLocals(mockUser({ roleId: 2 }) as any);
|
||||||
|
|
||||||
const locals = createLocals(mockUser() as any);
|
|
||||||
const result = await invokeUpsert(upsertRequest({ roleId: '1' }), locals);
|
const result = await invokeUpsert(upsertRequest({ roleId: '1' }), locals);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
|
|||||||
Reference in New Issue
Block a user