refactor(shared): extract hasWriteAll(locals) permission helper
The locals.user.groups.some(...WRITE_ALL) derivation was copy-pasted across the persons directory, persons review and the two document loaders touched by this PR. Extract a single tested hasWriteAll(locals) helper in $lib/shared/server and reuse it, removing the ad-hoc casts. Refs #667 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
30
frontend/src/lib/shared/server/permissions.spec.ts
Normal file
30
frontend/src/lib/shared/server/permissions.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { hasWriteAll } from './permissions';
|
||||||
|
|
||||||
|
type Locals = { user?: { groups?: { permissions: string[] }[] } };
|
||||||
|
|
||||||
|
const localsWith = (permissions: string[][]): Locals => ({
|
||||||
|
user: { groups: permissions.map((p) => ({ permissions: p })) }
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasWriteAll', () => {
|
||||||
|
it('returns true when a group grants WRITE_ALL', () => {
|
||||||
|
expect(hasWriteAll(localsWith([['READ_ALL', 'WRITE_ALL']]))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true when WRITE_ALL is in any of several groups', () => {
|
||||||
|
expect(hasWriteAll(localsWith([['READ_ALL'], ['WRITE_ALL']]))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when no group grants WRITE_ALL', () => {
|
||||||
|
expect(hasWriteAll(localsWith([['READ_ALL'], ['ANNOTATE_ALL']]))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for an anonymous user (no locals.user)', () => {
|
||||||
|
expect(hasWriteAll({})).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when the user has no groups', () => {
|
||||||
|
expect(hasWriteAll({ user: {} })).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
14
frontend/src/lib/shared/server/permissions.ts
Normal file
14
frontend/src/lib/shared/server/permissions.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Server-side permission predicates derived from the authenticated user in `locals`.
|
||||||
|
*
|
||||||
|
* The user shape is intentionally narrowed to the only field these checks read
|
||||||
|
* (`groups[].permissions`) so the helper works against `App.Locals` without importing it.
|
||||||
|
*/
|
||||||
|
type PermissionLocals = {
|
||||||
|
user?: { groups?: { permissions: string[] }[] } | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** True when any of the user's groups grants WRITE_ALL. False for anonymous users. */
|
||||||
|
export function hasWriteAll(locals: PermissionLocals): boolean {
|
||||||
|
return locals.user?.groups?.some((group) => group.permissions.includes('WRITE_ALL')) ?? false;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { error, fail, redirect } from '@sveltejs/kit';
|
|||||||
import { env } from '$env/dynamic/private';
|
import { env } from '$env/dynamic/private';
|
||||||
import { createApiClient, extractErrorCode } from '$lib/shared/api.server';
|
import { createApiClient, extractErrorCode } from '$lib/shared/api.server';
|
||||||
import { parseBackendError, getErrorMessage } from '$lib/shared/errors';
|
import { parseBackendError, getErrorMessage } from '$lib/shared/errors';
|
||||||
|
import { hasWriteAll } from '$lib/shared/server/permissions';
|
||||||
|
|
||||||
export async function load({
|
export async function load({
|
||||||
params,
|
params,
|
||||||
@@ -15,11 +16,7 @@ export async function load({
|
|||||||
depends: (dep: string) => void;
|
depends: (dep: string) => void;
|
||||||
}) {
|
}) {
|
||||||
depends('app:document');
|
depends('app:document');
|
||||||
const canWrite =
|
if (!hasWriteAll(locals)) throw error(403, 'Forbidden');
|
||||||
locals.user?.groups?.some((g: { permissions: string[] }) =>
|
|
||||||
g.permissions.includes('WRITE_ALL')
|
|
||||||
) ?? false;
|
|
||||||
if (!canWrite) throw error(403, 'Forbidden');
|
|
||||||
|
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
const api = createApiClient(fetch);
|
const api = createApiClient(fetch);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { fail, redirect } from '@sveltejs/kit';
|
|||||||
import { env } from '$env/dynamic/private';
|
import { env } from '$env/dynamic/private';
|
||||||
import { createApiClient } from '$lib/shared/api.server';
|
import { createApiClient } from '$lib/shared/api.server';
|
||||||
import { parseBackendError, getErrorMessage } from '$lib/shared/errors';
|
import { parseBackendError, getErrorMessage } from '$lib/shared/errors';
|
||||||
|
import { hasWriteAll } from '$lib/shared/server/permissions';
|
||||||
|
|
||||||
export async function load({
|
export async function load({
|
||||||
fetch,
|
fetch,
|
||||||
@@ -12,11 +13,7 @@ export async function load({
|
|||||||
locals: App.Locals;
|
locals: App.Locals;
|
||||||
url: URL;
|
url: URL;
|
||||||
}) {
|
}) {
|
||||||
const canWrite =
|
if (!hasWriteAll(locals)) throw redirect(303, '/');
|
||||||
locals.user?.groups?.some((g: { permissions: string[] }) =>
|
|
||||||
g.permissions.includes('WRITE_ALL')
|
|
||||||
) ?? false;
|
|
||||||
if (!canWrite) throw redirect(303, '/');
|
|
||||||
|
|
||||||
const senderId = url.searchParams.get('senderId') || '';
|
const senderId = url.searchParams.get('senderId') || '';
|
||||||
const receiverId = url.searchParams.get('receiverId') || '';
|
const receiverId = url.searchParams.get('receiverId') || '';
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import { createApiClient } from '$lib/shared/api.server';
|
import { createApiClient } from '$lib/shared/api.server';
|
||||||
import { getErrorMessage } from '$lib/shared/errors';
|
import { getErrorMessage } from '$lib/shared/errors';
|
||||||
|
import { hasWriteAll } from '$lib/shared/server/permissions';
|
||||||
|
|
||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
|
|
||||||
@@ -21,10 +22,7 @@ export async function load({ url, fetch, locals }) {
|
|||||||
|
|
||||||
const api = createApiClient(fetch);
|
const api = createApiClient(fetch);
|
||||||
|
|
||||||
const canWrite =
|
const canWrite = hasWriteAll(locals);
|
||||||
(locals.user as { groups?: { permissions: string[] }[] } | undefined)?.groups?.some((g) =>
|
|
||||||
g.permissions.includes('WRITE_ALL')
|
|
||||||
) ?? false;
|
|
||||||
|
|
||||||
const filters = {
|
const filters = {
|
||||||
q: q || undefined,
|
q: q || undefined,
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { error, fail } from '@sveltejs/kit';
|
import { error, fail } from '@sveltejs/kit';
|
||||||
import { createApiClient, extractErrorCode } from '$lib/shared/api.server';
|
import { createApiClient, extractErrorCode } from '$lib/shared/api.server';
|
||||||
import { getErrorMessage } from '$lib/shared/errors';
|
import { getErrorMessage } from '$lib/shared/errors';
|
||||||
|
import { hasWriteAll } from '$lib/shared/server/permissions';
|
||||||
|
|
||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
|
|
||||||
export async function load({ url, fetch, locals }) {
|
export async function load({ url, fetch, locals }) {
|
||||||
const canWrite =
|
const canWrite = hasWriteAll(locals);
|
||||||
(locals.user as { groups?: { permissions: string[] }[] } | undefined)?.groups?.some((g) =>
|
|
||||||
g.permissions.includes('WRITE_ALL')
|
|
||||||
) ?? false;
|
|
||||||
|
|
||||||
const page = Math.max(0, Number.parseInt(url.searchParams.get('page') ?? '0', 10) || 0);
|
const page = Math.max(0, Number.parseInt(url.searchParams.get('page') ?? '0', 10) || 0);
|
||||||
const api = createApiClient(fetch);
|
const api = createApiClient(fetch);
|
||||||
|
|||||||
Reference in New Issue
Block a user