Remove "User Roles" admin page

This commit is contained in:
NeilClack
2026-09-11 12:44:25 -05:00
parent fa614b8c37
commit 020289966d
5 changed files with 0 additions and 260 deletions
-1
View File
@@ -45,7 +45,6 @@ CRUD layer has been removed; see `docs/ADMIN_OVERHAUL.md` for the migration note
| -------------------------------- | ------------------------ | | -------------------------------- | ------------------------ |
| `/admin/users` | Name, Email, Role | | `/admin/users` | Name, Email, Role |
| `/admin/profiles` | Reference implementation | | `/admin/profiles` | Reference implementation |
| `/admin/user-roles` | |
| `/admin/proclamations` | Super-only content edits | | `/admin/proclamations` | Super-only content edits |
| `/admin/solfeggio-frequencies` | | | `/admin/solfeggio-frequencies` | |
| `/admin/proclamation-categories` | Draggable reorder | | `/admin/proclamation-categories` | Draggable reorder |
@@ -3,21 +3,17 @@ import { createLocals, createFormRequest, mockUser } from '$lib/test-utils';
vi.mock('$lib/db', () => ({ vi.mock('$lib/db', () => ({
prisma: { prisma: {
userRole: { create: vi.fn(), update: vi.fn(), delete: vi.fn() },
solfeggioFrequency: { 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() }, 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() } 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 frequencies } from './solfeggio-frequencies/+page.server';
import { actions as categories } from './proclamation-categories/+page.server'; import { actions as categories } from './proclamation-categories/+page.server';
import { actions as groups } from './proclamation-groups/+page.server'; import { actions as groups } from './proclamation-groups/+page.server';
const staff = () => createLocals(mockUser({ roleId: 3 }) as any); 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 user = () => createLocals(mockUser({ roleId: 4 }) as any);
const anonymous = () => createLocals(); const anonymous = () => createLocals();
@@ -27,7 +23,6 @@ type CrudActions = {
}; };
const pages: Array<[string, CrudActions]> = [ const pages: Array<[string, CrudActions]> = [
['user-roles', userRoles as unknown as CrudActions],
['solfeggio-frequencies', frequencies as unknown as CrudActions], ['solfeggio-frequencies', frequencies as unknown as CrudActions],
['proclamation-categories', categories as unknown as CrudActions], ['proclamation-categories', categories as unknown as CrudActions],
['proclamation-groups', groups as unknown as CrudActions] ['proclamation-groups', groups as unknown as CrudActions]
@@ -60,25 +55,3 @@ describe('admin CRUD action role guards', () => {
}); });
} }
}); });
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,71 +0,0 @@
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';
import { superFormErrorsToFieldErrors } from '$lib/superform-errors';
const UserRoleSchema = z.object({
name: z.string().min(1, 'Required').max(100, 'Too long')
});
export const load: PageServerLoad = async () => {
const records = await prisma.userRole.findMany({ orderBy: { id: 'asc' } });
return { records: records as unknown as Record<string, unknown>[] };
};
export const actions = {
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(UserRoleSchema));
if (!form.valid) {
const { error: formError, fieldErrors } = superFormErrorsToFieldErrors(
form.errors as Record<string, unknown>
);
return { success: false, action: 'upsert', error: formError, fieldErrors };
}
const rawId = formData.get('id')?.toString();
const data = { name: form.data.name };
try {
if (rawId) {
await prisma.userRole.update({ where: { id: Number(rawId) }, data });
} else {
await prisma.userRole.create({ data });
}
} catch (e: unknown) {
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2002') {
return { success: false, action: 'upsert', error: 'A role with that name already exists.' };
}
throw e;
}
return { success: true, action: 'upsert' };
},
delete: async ({ request, locals }) => {
if (!isAdmin(locals)) {
return { success: false, error: 'Not authorized' };
}
const formData = await request.formData();
const rawId = formData.get('id')?.toString();
if (!rawId) return { success: false, error: 'Missing id' };
try {
await prisma.userRole.delete({ where: { id: Number(rawId) } });
} catch (e: unknown) {
if (e && typeof e === 'object' && 'code' in e && (e as { code: string }).code === 'P2003') {
return { success: false, error: 'Cannot delete: this role is assigned to users.' };
}
throw e;
}
return { success: true, action: 'delete' };
}
};
@@ -1,160 +0,0 @@
<script lang="ts">
import { enhance } from '$app/forms';
import type { PageProps } from './$types';
import DataTable, { type DataTableColumn } from '$lib/components/DataTable.svelte';
import AdminModal from '$lib/components/admin/AdminModal.svelte';
type ActionResult = {
success?: boolean;
action?: string;
error?: string;
fieldErrors?: Record<string, string>;
} | null;
let { data, form }: PageProps = $props();
let canDelete = $derived((data.adminUser?.roleId ?? 4) <= 2);
const columns: DataTableColumn[] = [
{ label: 'Id', value: (r) => r.id },
{ label: 'Name', value: (r) => r.name }
];
let modalOpen = $state(false);
let editingRecord = $state<Record<string, unknown> | null>(null);
let name = $state('');
let formError = $state('');
let fieldErrors = $state<Record<string, string>>({});
let successMessage = $state('');
let lastForm = $state.raw<ActionResult>(null);
let closeTimer: ReturnType<typeof setTimeout> | undefined;
function clearCloseTimer() {
if (closeTimer) {
clearTimeout(closeTimer);
closeTimer = undefined;
}
}
function resetModalState() {
clearCloseTimer();
formError = '';
fieldErrors = {};
successMessage = '';
}
$effect(() => {
if (!form || form === lastForm) return;
lastForm = form;
if (form.success) {
formError = '';
fieldErrors = {};
successMessage = editingRecord ? 'Role updated' : 'Role created';
closeTimer = setTimeout(() => {
modalOpen = false;
successMessage = '';
closeTimer = undefined;
}, 1200);
} else {
successMessage = '';
formError = form.error ?? '';
fieldErrors = form.fieldErrors ?? {};
}
});
function openCreate() {
resetModalState();
editingRecord = null;
name = '';
lastForm = form;
modalOpen = true;
}
function openEdit(record: Record<string, unknown>) {
resetModalState();
editingRecord = record;
name = String(record.name ?? '');
lastForm = form;
modalOpen = true;
}
function closeModal() {
clearCloseTimer();
modalOpen = false;
editingRecord = null;
}
</script>
<div>
<DataTable
title="User Roles"
{columns}
records={data.records}
newLabel="New Role"
searchPlaceholder="Search roles..."
onRowClick={openEdit}
onNew={openCreate}
/>
</div>
{#if modalOpen}
<AdminModal
title={editingRecord ? 'Edit User Role' : 'Create User Role'}
open={modalOpen}
onClose={closeModal}
{canDelete}
recordId={editingRecord?.id as string | undefined}
>
{#if successMessage}
<div
class="mb-4 rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700"
>
{successMessage}
</div>
{/if}
{#if formError}
<div class="mb-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
{formError}
</div>
{/if}
<form method="POST" action="?/upsert" use:enhance class="flex flex-col gap-4">
{#if editingRecord}
<input type="hidden" name="id" value={String(editingRecord.id)} />
{/if}
<div>
<label for="field-name" class="mb-1 block text-sm font-medium text-slate-700">Name</label>
<input
id="field-name"
type="text"
name="name"
required
bind:value={name}
class="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-indigo-500 focus:outline-none"
/>
{#if fieldErrors.name}
<p class="mt-1 text-sm text-red-600">{fieldErrors.name}</p>
{/if}
</div>
<div class="mt-2 flex justify-end gap-3">
<button
type="button"
onclick={closeModal}
class="rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-50"
>
Cancel
</button>
<button
type="submit"
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-indigo-700"
>
{editingRecord ? 'Save Role' : 'Create Role'}
</button>
</div>
</form>
</AdminModal>
{/if}
-1
View File
@@ -17,7 +17,6 @@
const adminModels = [ const adminModels = [
{ slug: 'users', href: '/admin/users', displayName: 'Users' }, { slug: 'users', href: '/admin/users', displayName: 'Users' },
{ slug: 'profiles', href: '/admin/profiles', displayName: 'Profiles' }, { slug: 'profiles', href: '/admin/profiles', displayName: 'Profiles' },
{ slug: 'user-roles', href: '/admin/user-roles', displayName: 'User Roles' },
{ {
slug: 'proclamations', slug: 'proclamations',
href: '/admin/proclamations', href: '/admin/proclamations',