Restrict proclamation editing and content to Super admins only
- Only roleId <= 1 (SUPER) can create/edit/delete proclamations - Content field is stripped from records for non-Super users - Content field is hidden from the form for non-Super users - Server actions reject upsert/delete from non-Super users
This commit is contained in:
@@ -8,9 +8,24 @@ import * as Schemas from '$lib/svelte-admin/schemas';
|
||||
|
||||
const config = getModelConfig('proclamations')!;
|
||||
|
||||
function viewerRoleId(locals: App.Locals): number {
|
||||
return (locals.user as { roleId?: number } | null)?.roleId ?? 4;
|
||||
}
|
||||
|
||||
function isSuper(locals: App.Locals): boolean {
|
||||
return viewerRoleId(locals) <= 1;
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
const base = await loadRecords(locals, config);
|
||||
|
||||
// Strip content from non-Super users
|
||||
if (!isSuper(locals)) {
|
||||
for (const record of base.records) {
|
||||
delete record.content;
|
||||
}
|
||||
}
|
||||
|
||||
const categories = await prisma.proclamationCategory.findMany({
|
||||
select: { id: true, name: true, parentId: true },
|
||||
orderBy: { name: 'asc' }
|
||||
@@ -25,6 +40,10 @@ export const actions = {
|
||||
return { success: false, error: 'Unauthorized' };
|
||||
}
|
||||
|
||||
if (!isSuper(locals)) {
|
||||
return { success: false, error: 'Only Super admins can edit proclamations' };
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const form = await superValidate(formData, zod4(Schemas.Proclamation));
|
||||
if (!form.valid) {
|
||||
@@ -70,6 +89,9 @@ export const actions = {
|
||||
return { success: true };
|
||||
},
|
||||
delete: async ({ request, locals }) => {
|
||||
if (!isSuper(locals)) {
|
||||
return { success: false, error: 'Only Super admins can delete proclamations' };
|
||||
}
|
||||
return deleteRecord(request, config, locals);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
let { data, form }: PageProps = $props();
|
||||
|
||||
const config = getModelConfig('proclamations')!;
|
||||
const canDelete = $derived((data.adminUser?.roleId ?? 4) <= 2);
|
||||
const viewerRoleId = $derived(data.adminUser?.roleId ?? 4);
|
||||
const isSuper = $derived(viewerRoleId <= 1);
|
||||
const canDelete = $derived(viewerRoleId <= 2);
|
||||
|
||||
let modalOpen = $state(false);
|
||||
let editingRecord = $state<Record<string, unknown> | null>(null);
|
||||
@@ -63,8 +65,8 @@
|
||||
searchFields={config.searchFields}
|
||||
fieldTypes={config.fieldTypes}
|
||||
singular={config.displayNameSingular}
|
||||
onRowClick={openEdit}
|
||||
onNew={openCreate}
|
||||
onRowClick={isSuper ? openEdit : undefined}
|
||||
onNew={isSuper ? openCreate : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -83,6 +85,7 @@
|
||||
categories={data.categories}
|
||||
{fieldErrors}
|
||||
{formError}
|
||||
{isSuper}
|
||||
submitLabel={editingRecord ? 'Update' : 'Create'}
|
||||
/>
|
||||
</AdminModal>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
formError = '',
|
||||
relationOptionsData = {} as Record<string, { label: string; value: string | number }[]>,
|
||||
categories = [] as { id: number; name: string; parentId: number | null }[],
|
||||
isSuper = false,
|
||||
action = '?/upsert',
|
||||
submitLabel = 'Save'
|
||||
}: {
|
||||
@@ -16,6 +17,7 @@
|
||||
formError?: string;
|
||||
relationOptionsData?: Record<string, { label: string; value: string | number }[]>;
|
||||
categories?: { id: number; name: string; parentId: number | null }[];
|
||||
isSuper?: boolean;
|
||||
action?: string;
|
||||
submitLabel?: string;
|
||||
} = $props();
|
||||
@@ -190,23 +192,25 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="field-content"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Content
|
||||
</label>
|
||||
<div id="field-content">
|
||||
<TipTap
|
||||
initialContent={typeof record.content === 'string' ? record.content : ''}
|
||||
name="content"
|
||||
/>
|
||||
{#if isSuper}
|
||||
<div>
|
||||
<label
|
||||
for="field-content"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Content
|
||||
</label>
|
||||
<div id="field-content">
|
||||
<TipTap
|
||||
initialContent={typeof record.content === 'string' ? record.content : ''}
|
||||
name="content"
|
||||
/>
|
||||
</div>
|
||||
{#if fieldErrors.content}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.content}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#if fieldErrors.content}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.content}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user