From 9aa810f20df1e612e0b0331c61cbb866a82c0616 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 28 Oct 2026 04:46:45 +0100 Subject: [PATCH] refactor: keep the share rights presets in one module for every share dialog --- .../settings/share-collection-dialog.tsx | 123 +----------------- lib/jmap/share-rights.ts | 102 +++++++++++++++ 2 files changed, 104 insertions(+), 121 deletions(-) create mode 100644 lib/jmap/share-rights.ts diff --git a/components/settings/share-collection-dialog.tsx b/components/settings/share-collection-dialog.tsx index e007e0c22..437da3f19 100644 --- a/components/settings/share-collection-dialog.tsx +++ b/components/settings/share-collection-dialog.tsx @@ -6,130 +6,11 @@ import { Button } from "@/components/ui/button"; import { Avatar } from "@/components/ui/avatar"; import { X, Loader2, UserPlus, Trash2, Users, ChevronDown } from "@/components/icons"; import type { IJMAPClient } from "@/lib/jmap/client-interface"; -import type { Principal, PrincipalDirectoryStatus, CalendarRights, AddressBookRights, FileNodeRights, MailboxRights } from "@/lib/jmap/types"; +import type { Principal, PrincipalDirectoryStatus } from "@/lib/jmap/types"; +import { detectPreset, presetRights, type AnyRights, type RolePreset, type ShareKind } from "@/lib/jmap/share-rights"; import { loadPrincipalDirectory } from "@/lib/jmap/principal-directory"; import { toast } from "@/stores/toast-store"; -type ShareKind = "calendar" | "addressBook" | "file" | "mailbox"; -type AnyRights = CalendarRights | AddressBookRights | FileNodeRights | MailboxRights; - -type RolePreset = "freeBusy" | "read" | "readWrite" | "manager" | "custom"; - -const CALENDAR_PRESETS: Record, CalendarRights> = { - freeBusy: { - mayReadFreeBusy: true, mayReadItems: false, mayWriteAll: false, mayWriteOwn: false, - mayUpdatePrivate: false, mayRSVP: false, mayShare: false, mayDelete: false, - }, - read: { - mayReadFreeBusy: true, mayReadItems: true, mayWriteAll: false, mayWriteOwn: false, - mayUpdatePrivate: false, mayRSVP: false, mayShare: false, mayDelete: false, - }, - readWrite: { - mayReadFreeBusy: true, mayReadItems: true, mayWriteAll: true, mayWriteOwn: true, - mayUpdatePrivate: true, mayRSVP: true, mayShare: false, mayDelete: false, - }, - manager: { - mayReadFreeBusy: true, mayReadItems: true, mayWriteAll: true, mayWriteOwn: true, - mayUpdatePrivate: true, mayRSVP: true, mayShare: true, mayDelete: true, - }, -}; - -const ADDRESS_BOOK_PRESETS: Record, AddressBookRights> = { - read: { mayRead: true, mayWrite: false, mayShare: false, mayDelete: false }, - readWrite: { mayRead: true, mayWrite: true, mayShare: false, mayDelete: false }, - manager: { mayRead: true, mayWrite: true, mayShare: true, mayDelete: true }, -}; - -const FILE_PRESETS: Record, FileNodeRights> = { - read: { - mayRead: true, mayAddChildren: false, mayRename: false, - mayDelete: false, mayModifyContent: false, mayShare: false, - }, - readWrite: { - mayRead: true, mayAddChildren: true, mayRename: true, - mayDelete: true, mayModifyContent: true, mayShare: false, - }, - manager: { - mayRead: true, mayAddChildren: true, mayRename: true, - mayDelete: true, mayModifyContent: true, mayShare: true, - }, -}; - -// Mail folder rights (RFC 8621 §2 + mail:share). "Read & write" lets the -// grantee file, flag and remove mail; "Manager" additionally lets them -// rename/delete the folder, create subfolders, send as it and re-share. -const MAILBOX_PRESETS: Record, MailboxRights> = { - read: { - mayReadItems: true, mayAddItems: false, mayRemoveItems: false, maySetSeen: true, - maySetKeywords: false, mayCreateChild: false, mayRename: false, mayDelete: false, - maySubmit: false, mayShare: false, - }, - readWrite: { - mayReadItems: true, mayAddItems: true, mayRemoveItems: true, maySetSeen: true, - maySetKeywords: true, mayCreateChild: false, mayRename: false, mayDelete: false, - maySubmit: false, mayShare: false, - }, - manager: { - mayReadItems: true, mayAddItems: true, mayRemoveItems: true, maySetSeen: true, - maySetKeywords: true, mayCreateChild: true, mayRename: true, mayDelete: true, - maySubmit: true, mayShare: true, - }, -}; - -function detectMailboxPreset(r: MailboxRights): RolePreset { - for (const [name, preset] of Object.entries(MAILBOX_PRESETS) as [Exclude, MailboxRights][]) { - const keys = Object.keys(preset) as (keyof MailboxRights)[]; - if (keys.every((k) => preset[k] === (r[k] ?? false))) { - return name; - } - } - return "custom"; -} - -function detectCalendarPreset(r: CalendarRights): RolePreset { - for (const [name, preset] of Object.entries(CALENDAR_PRESETS) as [Exclude, CalendarRights][]) { - if ((Object.keys(preset) as (keyof CalendarRights)[]).every((k) => preset[k] === r[k])) { - return name; - } - } - return "custom"; -} - -function detectAddressBookPreset(r: AddressBookRights): RolePreset { - for (const [name, preset] of Object.entries(ADDRESS_BOOK_PRESETS) as [Exclude, AddressBookRights][]) { - const keys = Object.keys(preset) as (keyof AddressBookRights)[]; - if (keys.every((k) => preset[k] === (r[k] ?? false))) { - return name; - } - } - return "custom"; -} - -function detectFilePreset(r: FileNodeRights): RolePreset { - for (const [name, preset] of Object.entries(FILE_PRESETS) as [Exclude, FileNodeRights][]) { - const keys = Object.keys(preset) as (keyof FileNodeRights)[]; - if (keys.every((k) => preset[k] === (r[k] ?? false))) { - return name; - } - } - return "custom"; -} - -function presetRights(kind: ShareKind, preset: RolePreset): AnyRights | undefined { - if (preset === "custom") return undefined; - if (kind === "calendar") return CALENDAR_PRESETS[preset as keyof typeof CALENDAR_PRESETS]; - if (kind === "file") return FILE_PRESETS[preset as keyof typeof FILE_PRESETS]; - if (kind === "mailbox") return MAILBOX_PRESETS[preset as keyof typeof MAILBOX_PRESETS]; - return ADDRESS_BOOK_PRESETS[preset as keyof typeof ADDRESS_BOOK_PRESETS]; -} - -function detectPreset(kind: ShareKind, rights: AnyRights): RolePreset { - if (kind === "calendar") return detectCalendarPreset(rights as CalendarRights); - if (kind === "file") return detectFilePreset(rights as FileNodeRights); - if (kind === "mailbox") return detectMailboxPreset(rights as MailboxRights); - return detectAddressBookPreset(rights as AddressBookRights); -} - interface ShareCollectionDialogProps { client: IJMAPClient; kind: ShareKind; diff --git a/lib/jmap/share-rights.ts b/lib/jmap/share-rights.ts new file mode 100644 index 000000000..d7d52c581 --- /dev/null +++ b/lib/jmap/share-rights.ts @@ -0,0 +1,102 @@ +/** + * The rights Bulwark grants when it shares a folder, calendar, address book + * or file (`shareWith/`), as named presets, and the preset a + * stored grant amounts to. + */ + +import type { AddressBookRights, CalendarRights, FileNodeRights, MailboxRights } from './types'; + +export type ShareKind = 'calendar' | 'addressBook' | 'file' | 'mailbox'; +export type AnyRights = CalendarRights | AddressBookRights | FileNodeRights | MailboxRights; +export type RolePreset = 'freeBusy' | 'read' | 'readWrite' | 'manager' | 'custom'; +type NamedPreset = Exclude; + +export const CALENDAR_PRESETS: Record, CalendarRights> = { + freeBusy: { + mayReadFreeBusy: true, mayReadItems: false, mayWriteAll: false, mayWriteOwn: false, + mayUpdatePrivate: false, mayRSVP: false, mayShare: false, mayDelete: false, + }, + read: { + mayReadFreeBusy: true, mayReadItems: true, mayWriteAll: false, mayWriteOwn: false, + mayUpdatePrivate: false, mayRSVP: false, mayShare: false, mayDelete: false, + }, + readWrite: { + mayReadFreeBusy: true, mayReadItems: true, mayWriteAll: true, mayWriteOwn: true, + mayUpdatePrivate: true, mayRSVP: true, mayShare: false, mayDelete: false, + }, + manager: { + mayReadFreeBusy: true, mayReadItems: true, mayWriteAll: true, mayWriteOwn: true, + mayUpdatePrivate: true, mayRSVP: true, mayShare: true, mayDelete: true, + }, +}; + +export const ADDRESS_BOOK_PRESETS: Record = { + read: { mayRead: true, mayWrite: false, mayShare: false, mayDelete: false }, + readWrite: { mayRead: true, mayWrite: true, mayShare: false, mayDelete: false }, + manager: { mayRead: true, mayWrite: true, mayShare: true, mayDelete: true }, +}; + +export const FILE_PRESETS: Record = { + read: { + mayRead: true, mayAddChildren: false, mayRename: false, + mayDelete: false, mayModifyContent: false, mayShare: false, + }, + readWrite: { + mayRead: true, mayAddChildren: true, mayRename: true, + mayDelete: true, mayModifyContent: true, mayShare: false, + }, + manager: { + mayRead: true, mayAddChildren: true, mayRename: true, + mayDelete: true, mayModifyContent: true, mayShare: true, + }, +}; + +// Mail folder rights (RFC 8621 §2 + mail:share). "Read & write" lets the +// grantee file, flag and remove mail; "Manager" additionally lets them +// rename/delete the folder, create subfolders, send as it and re-share. +export const MAILBOX_PRESETS: Record = { + read: { + mayReadItems: true, mayAddItems: false, mayRemoveItems: false, maySetSeen: true, + maySetKeywords: false, mayCreateChild: false, mayRename: false, mayDelete: false, + maySubmit: false, mayShare: false, + }, + readWrite: { + mayReadItems: true, mayAddItems: true, mayRemoveItems: true, maySetSeen: true, + maySetKeywords: true, mayCreateChild: false, mayRename: false, mayDelete: false, + maySubmit: false, mayShare: false, + }, + manager: { + mayReadItems: true, mayAddItems: true, mayRemoveItems: true, maySetSeen: true, + maySetKeywords: true, mayCreateChild: true, mayRename: true, mayDelete: true, + maySubmit: true, mayShare: true, + }, +}; + +function presetsFor(kind: ShareKind): Partial> { + if (kind === 'calendar') return CALENDAR_PRESETS; + if (kind === 'file') return FILE_PRESETS; + if (kind === 'mailbox') return MAILBOX_PRESETS; + return ADDRESS_BOOK_PRESETS; +} + +function matches(preset: AnyRights, rights: AnyRights): boolean { + const have = rights as unknown as Record; + return Object.entries(preset).every(([key, want]) => { + const got = have[key] ?? false; + return got === want; + }); +} + +/** The rights a preset grants; undefined for "custom". */ +export function presetRights(kind: ShareKind, preset: RolePreset): AnyRights | undefined { + if (preset === 'custom') return undefined; + return presetsFor(kind)[preset]; +} + +/** The preset a stored grant amounts to, or "custom". */ +export function detectPreset(kind: ShareKind, rights: AnyRights): RolePreset { + for (const [name, preset] of Object.entries(presetsFor(kind)) as [RolePreset, AnyRights][]) { + if (matches(preset, rights)) return name; + } + return 'custom'; +}