dorfteich/apps/web/src/access/use-access-rules.ts
Claude Opus 4.8 406886c56c
Some checks failed
CD / Build and push images (push) Successful in 3m5s
CI / Lint, typecheck, test (push) Successful in 2m31s
CI / Auth e2e pack (push) Failing after 2m0s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 12s
Add label- and page-scope access rules UI including deny (#55)
Pond Admins configure the vision's fine-grained cases through a plain-language
surface, on top of the base roles from #54.

- shared: `AccessRuleView` (a grant enriched with subject/scope display names)
  and pure conflict helpers `scopeSpecificity`/`sameGrantSubject`/
  `isRuleShadowed` (unit-tested) for the client-side shadowed-rule hint. New
  `access` i18n namespace (de+en) with sentence templates (ADR 0012).
- api: `GET /ponds/:id/grants/access-rules` (Pond-Admin) returns the pond's
  grants enriched with each user's display name and each label/page scope's
  name, resolved in one batched query per kind.
- web `access/`: `AccessRulesManager` in Pond Settings — the pond's rules
  grouped by subject and rendered as readable de/en sentences ("Anna may not
  edit pages labeled “Confidential”"), an add form (subject = member or the
  `signed-in`/`public` pseudo-subjects; scope = label from the tree or a
  specific page; role; allow/deny) that warns when a rule would be shadowed by
  a more specific existing one (shared algorithm) and requires an explicit
  confirmation before granting anything to `public`. Semantics are the shared
  resolver's — the UI only reflects permissions.md.
- tests: shared `conflicts.test.ts`; an api db case for the enriched endpoint;
  a browser `access-rules` pack that configures BOTH vision patterns through
  the UI and verifies their effect end to end — "deny label X" (an editor
  loses a labelled page) and "only label Y" (a signed-in non-member, new
  `fixture-viewer`, reads only the labelled pages) — plus the shadow hint and
  the public confirmation, with its own CI step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
2026-07-09 21:45:31 +02:00

39 lines
1.4 KiB
TypeScript

import type { AccessRuleView, CreateGrantInput } from '@dorfteich/shared';
import { useQuery, useQueryClient, type UseQueryResult } from '@tanstack/react-query';
import { apiDelete, apiGet, apiPost } from '../lib/api';
/** Query key for a pond's enriched access rules (issue #55). */
export const accessRulesKey = (pondId: string): string[] => ['access-rules', pondId];
/** The pond's grants enriched with subject/scope names (Pond-Admin only). */
export function useAccessRules(pondId: string | undefined): UseQueryResult<AccessRuleView[]> {
return useQuery({
queryKey: accessRulesKey(pondId ?? ''),
queryFn: () => apiGet<AccessRuleView[]>(`/ponds/${pondId}/grants/access-rules`),
enabled: Boolean(pondId),
retry: false, // a non-admin gets 403 — do not hammer the endpoint
});
}
/** Add/remove access-rule grants; each refreshes the rule list. */
export function useAccessRuleMutations(pondId: string): {
add: (input: CreateGrantInput) => Promise<void>;
remove: (grantId: string) => Promise<void>;
} {
const queryClient = useQueryClient();
const invalidate = (): Promise<void> =>
queryClient.invalidateQueries({ queryKey: accessRulesKey(pondId) });
return {
add: async (input) => {
await apiPost(`/ponds/${pondId}/grants`, input);
await invalidate();
},
remove: async (grantId) => {
await apiDelete(`/ponds/${pondId}/grants/${grantId}`);
await invalidate();
},
};
}