@rapidmx/web-client 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/apps/admin/audit-log/index.tsx +35 -8
  2. package/apps/admin/branding/index.tsx +39 -404
  3. package/apps/admin/data-requests/index.tsx +480 -485
  4. package/apps/admin/distribution-lists/[uid].tsx +19 -1
  5. package/apps/admin/domains/[uid].tsx +92 -259
  6. package/apps/admin/encryption-policy/index.tsx +19 -0
  7. package/apps/admin/escrow-scopes/[uid].tsx +348 -237
  8. package/apps/admin/escrow-scopes/new/index.tsx +8 -2
  9. package/apps/admin/index.tsx +118 -82
  10. package/apps/admin/ingest-queue/index.tsx +44 -5
  11. package/apps/admin/mailbox-policy/index.tsx +19 -0
  12. package/apps/admin/mailboxes/[uid].tsx +211 -184
  13. package/apps/admin/mailboxes/new/index.tsx +28 -291
  14. package/apps/admin/plugins/index.tsx +15 -0
  15. package/apps/admin/quarantine/index.tsx +46 -14
  16. package/apps/admin/retention-policy/index.tsx +39 -132
  17. package/apps/admin/setup/index.tsx +15 -0
  18. package/apps/admin/transport-rules/[uid].tsx +30 -1
  19. package/apps/admin/transport-rules/_transportRuleConfig.tsx +38 -2
  20. package/apps/admin/transport-rules/index.tsx +24 -6
  21. package/apps/admin/transport-rules/new/index.tsx +30 -1
  22. package/apps/escrow/_layout.tsx +2 -3
  23. package/apps/escrow/audit-log/index.tsx +196 -168
  24. package/apps/escrow/matters/[uid].tsx +621 -521
  25. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +22 -1
  26. package/apps/shared/components/admin/layout/AdminShell.tsx +251 -210
  27. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -0
  28. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  29. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +55 -11
  30. package/apps/shared/components/admin/settings/BrandingForm.tsx +385 -0
  31. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +187 -0
  32. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +112 -0
  33. package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +34 -0
  34. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +308 -0
  35. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +152 -0
  36. package/apps/shared/components/admin/settings/PluginsManager.tsx +1235 -0
  37. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +174 -0
  38. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +290 -0
  39. package/apps/shared/components/admin/setup/SetupWizard.tsx +416 -0
  40. package/apps/shared/components/admin/signOut.ts +79 -0
  41. package/apps/shared/components/admin/usePagedList.tsx +129 -0
  42. package/apps/shared/components/calendar/CalendarListSidebar.tsx +120 -76
  43. package/apps/shared/components/calendar/EventModal.tsx +717 -513
  44. package/apps/shared/components/calendar/MonthView.tsx +173 -139
  45. package/apps/shared/components/calendar/RecurrenceEditor.tsx +222 -180
  46. package/apps/shared/components/calendar/SplitDayView.tsx +143 -137
  47. package/apps/shared/components/calendar/TimeGridView.tsx +244 -198
  48. package/apps/shared/components/calendar/allDay.ts +124 -0
  49. package/apps/shared/components/calendar/layout/CalendarShell.tsx +80 -99
  50. package/apps/shared/components/contacts/ContactDetailPane.tsx +100 -21
  51. package/apps/shared/components/contacts/ContactForm.tsx +377 -314
  52. package/apps/shared/components/contacts/ContactsSidebar.tsx +6 -2
  53. package/apps/shared/components/contacts/KeyChangeReview.tsx +192 -0
  54. package/apps/shared/components/contacts/contactKeys.ts +78 -0
  55. package/apps/shared/components/escrow/layout/EscrowShell.tsx +148 -134
  56. package/apps/shared/components/layout/AppShell.tsx +270 -169
  57. package/apps/shared/components/layout/BrandingChrome.tsx +48 -10
  58. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +150 -26
  59. package/apps/shared/components/layout/MailboxProvisioning.tsx +19 -3
  60. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +350 -0
  61. package/apps/shared/components/layout/UnlockPromptProvider.tsx +257 -0
  62. package/apps/shared/components/mail/ConversationThreadPane.tsx +216 -181
  63. package/apps/shared/components/mail/MessageDetailPane.tsx +799 -76
  64. package/apps/shared/components/mail/compose/ComposeContext.tsx +26 -16
  65. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1652 -765
  66. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -0
  67. package/apps/shared/components/mail/layout/MailShell.tsx +418 -302
  68. package/apps/shared/components/mail/pinnedSigners.ts +124 -0
  69. package/apps/shared/components/mail/verificationSeals.ts +125 -0
  70. package/apps/shared/components/mail/writableMailboxes.ts +101 -0
  71. package/apps/shared/components/rules/RuleBuilder.tsx +311 -276
  72. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
  73. package/apps/shared/mail/findWellKnownFolderUid.ts +13 -0
  74. package/apps/shared/mail/listAllPages.ts +39 -0
  75. package/apps/shared/search/LocalIndexLifecycle.tsx +98 -0
  76. package/apps/shared/search/localIndexBlockCipher.ts +83 -0
  77. package/apps/shared/search/localIndexBuilder.ts +481 -0
  78. package/apps/shared/search/localIndexKey.ts +27 -0
  79. package/apps/shared/search/localIndexRpcClient.ts +316 -0
  80. package/apps/shared/search/localIndexSchema.ts +236 -0
  81. package/apps/shared/search/localIndexSizePreference.ts +88 -0
  82. package/apps/shared/search/localIndexStorage.ts +98 -0
  83. package/apps/shared/search/localIndexVFS.ts +266 -0
  84. package/apps/shared/search/localIndexWorker.ts +926 -0
  85. package/apps/shared/search/searchTier2.ts +79 -0
  86. package/apps/shared/search/wa-sqlite-shims.d.ts +44 -0
  87. package/apps/www/calendar/index.tsx +438 -407
  88. package/apps/www/contacts/[uid].tsx +21 -2
  89. package/apps/www/contacts/index.tsx +148 -17
  90. package/apps/www/index.tsx +1314 -486
  91. package/apps/www/messages/[uid].tsx +23 -7
  92. package/apps/www/settings/auto-reply/index.tsx +134 -129
  93. package/apps/www/settings/booking-types/[uid].tsx +306 -300
  94. package/apps/www/settings/encryption/index.tsx +800 -215
  95. package/apps/www/settings/filters/[uid].tsx +171 -155
  96. package/apps/www/settings/filters/new/index.tsx +8 -1
  97. package/apps/www/settings/focused-inbox/index.tsx +150 -148
  98. package/apps/www/settings/privacy/index.tsx +494 -436
  99. package/apps/www/settings/read-receipts/index.tsx +148 -125
  100. package/apps/www/settings/sharing/index.tsx +274 -0
  101. package/apps/www/tasks/index.tsx +638 -530
  102. package/dist/apps/admin/audit-log/index.js +38 -8
  103. package/dist/apps/admin/branding/index.js +4 -98
  104. package/dist/apps/admin/data-requests/index.js +52 -72
  105. package/dist/apps/admin/distribution-lists/[uid].js +1 -1
  106. package/dist/apps/admin/domains/[uid].js +5 -68
  107. package/dist/apps/admin/encryption-policy/index.js +8 -0
  108. package/dist/apps/admin/escrow-scopes/[uid].js +91 -12
  109. package/dist/apps/admin/escrow-scopes/new/index.js +8 -3
  110. package/dist/apps/admin/index.js +17 -1
  111. package/dist/apps/admin/ingest-queue/index.js +24 -6
  112. package/dist/apps/admin/mailbox-policy/index.js +8 -0
  113. package/dist/apps/admin/mailboxes/[uid].js +20 -4
  114. package/dist/apps/admin/mailboxes/new/index.js +4 -89
  115. package/dist/apps/admin/plugins/index.js +6 -0
  116. package/dist/apps/admin/quarantine/index.js +20 -11
  117. package/dist/apps/admin/retention-policy/index.js +3 -36
  118. package/dist/apps/admin/setup/index.js +6 -0
  119. package/dist/apps/admin/transport-rules/[uid].js +18 -2
  120. package/dist/apps/admin/transport-rules/_transportRuleConfig.js +19 -0
  121. package/dist/apps/admin/transport-rules/index.js +22 -6
  122. package/dist/apps/admin/transport-rules/new/index.js +18 -2
  123. package/dist/apps/escrow/_layout.js +3 -3
  124. package/dist/apps/escrow/audit-log/index.js +26 -1
  125. package/dist/apps/escrow/matters/[uid].js +77 -42
  126. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +11 -2
  127. package/dist/apps/shared/components/admin/layout/AdminShell.js +37 -4
  128. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +72 -0
  129. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  130. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +22 -4
  131. package/dist/apps/shared/components/admin/settings/BrandingForm.js +116 -0
  132. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +83 -0
  133. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +62 -0
  134. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +25 -0
  135. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +112 -0
  136. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +74 -0
  137. package/dist/apps/shared/components/admin/settings/PluginsManager.js +572 -0
  138. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +73 -0
  139. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +147 -0
  140. package/dist/apps/shared/components/admin/setup/SetupWizard.js +208 -0
  141. package/dist/apps/shared/components/admin/signOut.js +75 -0
  142. package/dist/apps/shared/components/admin/usePagedList.js +99 -0
  143. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +24 -13
  144. package/dist/apps/shared/components/calendar/EventModal.js +154 -21
  145. package/dist/apps/shared/components/calendar/MonthView.js +12 -5
  146. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +40 -5
  147. package/dist/apps/shared/components/calendar/SplitDayView.js +10 -4
  148. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -7
  149. package/dist/apps/shared/components/calendar/allDay.js +111 -0
  150. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +47 -39
  151. package/dist/apps/shared/components/contacts/ContactDetailPane.js +43 -5
  152. package/dist/apps/shared/components/contacts/ContactForm.js +45 -12
  153. package/dist/apps/shared/components/contacts/ContactsSidebar.js +2 -2
  154. package/dist/apps/shared/components/contacts/KeyChangeReview.js +63 -0
  155. package/dist/apps/shared/components/contacts/contactKeys.js +64 -0
  156. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +24 -5
  157. package/dist/apps/shared/components/layout/AppShell.js +100 -8
  158. package/dist/apps/shared/components/layout/BrandingChrome.js +45 -10
  159. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +63 -14
  160. package/dist/apps/shared/components/layout/MailboxProvisioning.js +10 -2
  161. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +174 -0
  162. package/dist/apps/shared/components/layout/UnlockPromptProvider.js +136 -0
  163. package/dist/apps/shared/components/mail/ConversationThreadPane.js +50 -12
  164. package/dist/apps/shared/components/mail/MessageDetailPane.js +464 -34
  165. package/dist/apps/shared/components/mail/compose/ComposeContext.js +9 -6
  166. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +807 -116
  167. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +50 -0
  168. package/dist/apps/shared/components/mail/layout/MailShell.js +116 -41
  169. package/dist/apps/shared/components/mail/pinnedSigners.js +98 -0
  170. package/dist/apps/shared/components/mail/verificationSeals.js +112 -0
  171. package/dist/apps/shared/components/mail/writableMailboxes.js +87 -0
  172. package/dist/apps/shared/components/rules/RuleBuilder.js +34 -6
  173. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
  174. package/dist/apps/shared/mail/findWellKnownFolderUid.js +12 -0
  175. package/dist/apps/shared/mail/listAllPages.js +25 -0
  176. package/dist/apps/shared/search/LocalIndexLifecycle.js +80 -0
  177. package/dist/apps/shared/search/localIndexBlockCipher.js +63 -0
  178. package/dist/apps/shared/search/localIndexBuilder.js +408 -0
  179. package/dist/apps/shared/search/localIndexKey.js +25 -0
  180. package/dist/apps/shared/search/localIndexRpcClient.js +241 -0
  181. package/dist/apps/shared/search/localIndexSchema.js +185 -0
  182. package/dist/apps/shared/search/localIndexSizePreference.js +78 -0
  183. package/dist/apps/shared/search/localIndexStorage.js +86 -0
  184. package/dist/apps/shared/search/localIndexVFS.js +258 -0
  185. package/dist/apps/shared/search/localIndexWorker.js +687 -0
  186. package/dist/apps/shared/search/searchTier2.js +38 -0
  187. package/dist/apps/www/calendar/index.js +38 -18
  188. package/dist/apps/www/contacts/[uid].js +13 -2
  189. package/dist/apps/www/contacts/index.js +94 -15
  190. package/dist/apps/www/index.js +770 -137
  191. package/dist/apps/www/messages/[uid].js +23 -7
  192. package/dist/apps/www/settings/auto-reply/index.js +9 -4
  193. package/dist/apps/www/settings/booking-types/[uid].js +17 -12
  194. package/dist/apps/www/settings/encryption/index.js +541 -96
  195. package/dist/apps/www/settings/filters/[uid].js +22 -7
  196. package/dist/apps/www/settings/filters/new/index.js +8 -2
  197. package/dist/apps/www/settings/focused-inbox/index.js +3 -1
  198. package/dist/apps/www/settings/privacy/index.js +57 -22
  199. package/dist/apps/www/settings/read-receipts/index.js +10 -3
  200. package/dist/apps/www/settings/sharing/index.js +124 -0
  201. package/dist/apps/www/tasks/index.js +76 -14
  202. package/package.json +3 -2
@@ -31,6 +31,8 @@ export interface EscrowScopeKeyAndHoldersFieldsProps {
31
31
  value: EscrowScopeKeyAndHoldersValue;
32
32
  onChange: (value: EscrowScopeKeyAndHoldersValue) => void;
33
33
  disabled?: boolean;
34
+ /** Shows the public key fields without letting them be edited, e.g. for keys generated in the browser. */
35
+ keyReadOnly?: boolean;
34
36
  }
35
37
 
36
38
  /** Builds an all-zeroes starting point for the "new" page — `notBefore`/`notAfter` default to "now" so an
@@ -49,7 +51,21 @@ export function emptyEscrowScopeKeyAndHoldersValue(): EscrowScopeKeyAndHoldersVa
49
51
  };
50
52
  }
51
53
 
52
- export default function EscrowScopeKeyAndHoldersFields({ value, onChange, disabled }: EscrowScopeKeyAndHoldersFieldsProps) {
54
+ /** The message shown when an administrator tries to make themselves a holder: whoever configures a scope must not
55
+ * also be able to approve access under it (separation of duties - the server refuses it too). Holders who were
56
+ * already on the scope (`existingHolderUserUids`) aren't treated as newly added. */
57
+ export function selfAsHolderError(
58
+ holderUserUids: string[],
59
+ adminUid: string | undefined,
60
+ existingHolderUserUids: string[] = [],
61
+ ): string | null {
62
+ if (!adminUid || !holderUserUids.includes(adminUid) || existingHolderUserUids.includes(adminUid)) {
63
+ return null;
64
+ }
65
+ return "You can't add yourself as a holder of an escrow scope you configure. Ask another administrator to add you, or choose different holders.";
66
+ }
67
+
68
+ export default function EscrowScopeKeyAndHoldersFields({ value, onChange, disabled, keyReadOnly }: EscrowScopeKeyAndHoldersFieldsProps) {
53
69
  function set<K extends keyof EscrowScopeKeyAndHoldersValue>(key: K, next: EscrowScopeKeyAndHoldersValue[K]) {
54
70
  onChange({ ...value, [key]: next });
55
71
  }
@@ -70,6 +86,7 @@ export default function EscrowScopeKeyAndHoldersFields({ value, onChange, disabl
70
86
  rows={4}
71
87
  value={value.publicKey}
72
88
  disabled={disabled}
89
+ readOnly={keyReadOnly}
73
90
  onChange={(e) => set("publicKey", e.target.value)}
74
91
  />
75
92
  </FormField>
@@ -81,6 +98,7 @@ export default function EscrowScopeKeyAndHoldersFields({ value, onChange, disabl
81
98
  className={INPUT_CLASS}
82
99
  value={value.keyType}
83
100
  disabled={disabled}
101
+ readOnly={keyReadOnly}
84
102
  onChange={(e) => set("keyType", e.target.value)}
85
103
  placeholder="x509"
86
104
  />
@@ -93,6 +111,7 @@ export default function EscrowScopeKeyAndHoldersFields({ value, onChange, disabl
93
111
  className={`${INPUT_CLASS} font-mono`}
94
112
  value={value.fingerprint}
95
113
  disabled={disabled}
114
+ readOnly={keyReadOnly}
96
115
  onChange={(e) => set("fingerprint", e.target.value)}
97
116
  />
98
117
  </FormField>
@@ -105,6 +124,7 @@ export default function EscrowScopeKeyAndHoldersFields({ value, onChange, disabl
105
124
  className={INPUT_CLASS}
106
125
  value={value.notBefore}
107
126
  disabled={disabled}
127
+ readOnly={keyReadOnly}
108
128
  onChange={(e) => set("notBefore", e.target.value)}
109
129
  />
110
130
  </FormField>
@@ -116,6 +136,7 @@ export default function EscrowScopeKeyAndHoldersFields({ value, onChange, disabl
116
136
  className={INPUT_CLASS}
117
137
  value={value.notAfter}
118
138
  disabled={disabled}
139
+ readOnly={keyReadOnly}
119
140
  onChange={(e) => set("notAfter", e.target.value)}
120
141
  />
121
142
  </FormField>
@@ -1,210 +1,251 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- import "../../../styles/app.css";
6
- import React, { PropsWithChildren, ReactNode, useEffect, useState } from "react";
7
- import {
8
- HiOutlineClipboardDocumentList,
9
- HiOutlineClock,
10
- HiOutlineDocumentArrowDown,
11
- HiOutlineGlobeAlt,
12
- HiOutlineInboxStack,
13
- HiOutlineKey,
14
- HiOutlinePaintBrush,
15
- HiOutlineQueueList,
16
- HiOutlineShieldCheck,
17
- HiOutlineShieldExclamation,
18
- HiOutlineUserGroup,
19
- } from "react-icons/hi2";
20
- import { apiFetch, ApiRequestError } from "@rapidmx/react-shared/util/api.js";
21
- import { useRedirectIfUnauthenticated } from "@rapidmx/react-shared/auth/session.js";
22
- import useBranding from "@rapidmx/react-shared/branding/useBranding.js";
23
- import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
24
- import BottomTabBar, { NavItem } from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
25
- import { BrandingFooter, BrandingHeader } from "../../layout/BrandingChrome.js";
26
- import UserMenu from "../../layout/UserMenu.js";
27
-
28
- export type AdminSection =
29
- | "mailboxes"
30
- | "quarantine"
31
- | "ingestQueue"
32
- | "domains"
33
- | "auditLog"
34
- | "distributionLists"
35
- | "transportRules"
36
- | "escrowScopes"
37
- | "retentionPolicy"
38
- | "dataRequests"
39
- | "branding";
40
-
41
- export interface AdminShellProps {
42
- /** Which icon in the rail is highlighted as the current section. */
43
- active: AdminSection;
44
- /** Populated automatically by the framework from an authenticated request (e.g. a valid `jwt` cookie). */
45
- userUid?: string;
46
- /** auth-server's base URL, injected via the route's `fetchProps` — see `src/mongo/routes/AdminConsoleRoute.ts`. */
47
- authServerUrl?: string;
48
- /**
49
- * Where `mailApi.ts`'s `impersonateUser()`/`stopImpersonating()` should call: the real auth-server in
50
- * production, or `""` under `yarn dev` to call this app's own local dev-only impersonation endpoint
51
- * instead — see `AdminConsoleRoute`'s `fetchProps` and `src/dev/DevImpersonationRoute.ts`.
52
- */
53
- impersonationBaseUrl?: string;
54
- }
55
-
56
- type Status = "checking" | "denied" | "error" | "authorized";
57
-
58
- /** Sections shown in the persistent icon rail / mobile tab bar — every admin area reachable from
59
- * anywhere in the console. Deliberately excludes `quarantine`/`ingestQueue`: those are scoped to a
60
- * single mailbox and only ever reached via links on that mailbox's own detail page (see
61
- * `apps/admin/mailboxes/[uid].tsx`), not global navigation destinations. */
62
- const NAV_ITEMS: NavItem[] = [
63
- { id: "mailboxes", href: "/admin", label: "Mailboxes", icon: HiOutlineInboxStack },
64
- { id: "domains", href: "/admin/domains", label: "Domains", icon: HiOutlineGlobeAlt },
65
- { id: "auditLog", href: "/admin/audit-log", label: "Audit Log", icon: HiOutlineClipboardDocumentList },
66
- {
67
- id: "distributionLists",
68
- href: "/admin/distribution-lists",
69
- label: "Distribution Lists",
70
- icon: HiOutlineUserGroup,
71
- },
72
- {
73
- id: "transportRules",
74
- href: "/admin/transport-rules",
75
- label: "Transport Rules",
76
- icon: HiOutlineShieldCheck,
77
- },
78
- {
79
- id: "escrowScopes",
80
- href: "/admin/escrow-scopes",
81
- label: "Escrow Scopes",
82
- icon: HiOutlineKey,
83
- },
84
- {
85
- id: "retentionPolicy",
86
- href: "/admin/retention-policy",
87
- label: "Retention Policy",
88
- icon: HiOutlineClock,
89
- },
90
- {
91
- id: "dataRequests",
92
- href: "/admin/data-requests",
93
- label: "Data Requests",
94
- icon: HiOutlineDocumentArrowDown,
95
- },
96
- { id: "branding", href: "/admin/branding", label: "Branding", icon: HiOutlinePaintBrush },
97
- ];
98
-
99
- /** Mailbox-scoped sections — not part of the global nav rail/tab bar (see `NAV_ITEMS` above), but
100
- * still need an entry here so the header can resolve a label for them when they're the active page. */
101
- const MAILBOX_SCOPED_ITEMS: NavItem[] = [
102
- { id: "quarantine", href: "/admin/quarantine", label: "Quarantine", icon: HiOutlineShieldExclamation },
103
- { id: "ingestQueue", href: "/admin/ingest-queue", label: "Ingest Queue", icon: HiOutlineQueueList },
104
- ];
105
-
106
- const ALL_ITEMS: NavItem[] = [...NAV_ITEMS, ...MAILBOX_SCOPED_ITEMS];
107
-
108
- /**
109
- * Gates every `apps/admin` page behind the `admin` trusted role. Uses `GET /api/admin/release-notes` (any
110
- * `BaseAdminRoute` endpoint works — this one is side-effect-free) purely as a canary: a 200 means the
111
- * caller's JWT carries a trusted role, a 403 means it doesn't. There is no local step-up/elevation flow
112
- * (that would need a cross-origin call to auth-server's own elevation endpoint — not wired up yet).
113
- */
114
- export default function AdminShell({ active, userUid, authServerUrl, children }: PropsWithChildren<AdminShellProps>) {
115
- const [status, setStatus] = useState<Status>("checking");
116
- const [error, setError] = useState<string | null>(null);
117
- const { branding, iconSrc } = useBranding();
118
-
119
- useRedirectIfUnauthenticated(userUid, authServerUrl);
120
-
121
- useEffect(() => {
122
- if (!userUid) {
123
- return;
124
- }
125
- apiFetch("/admin/release-notes")
126
- .then(() => setStatus("authorized"))
127
- .catch((err) => {
128
- if (err instanceof ApiRequestError && (err.status === 403 || err.status === 401)) {
129
- setStatus("denied");
130
- return;
131
- }
132
- setError(err instanceof ApiRequestError ? err.message : "Could not verify administrator access.");
133
- setStatus("error");
134
- });
135
- }, [userUid]);
136
-
137
- function handleSignOut() {
138
- window.location.href = authServerUrl ?? "/";
139
- }
140
-
141
- let content: ReactNode;
142
- if (!userUid || status === "checking") {
143
- content = <div className="min-h-screen" />;
144
- } else if (status === "denied") {
145
- content = (
146
- <div className="min-h-screen flex items-center justify-center p-8">
147
- <div className="w-full max-w-md">
148
- <Alert>You do not have administrator access.</Alert>
149
- </div>
150
- </div>
151
- );
152
- } else if (status === "error") {
153
- content = (
154
- <div className="min-h-screen flex items-center justify-center p-8">
155
- <div className="w-full max-w-md">
156
- <Alert>{error}</Alert>
157
- </div>
158
- </div>
159
- );
160
- } else {
161
- const activeItem = ALL_ITEMS.find((item) => item.id === active);
162
- content = (
163
- <div className="min-h-screen flex flex-col bg-surface-alt">
164
- <div className="flex-1 flex min-h-0">
165
- <nav
166
- aria-label="Admin sections"
167
- className="hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center py-3 gap-1"
168
- >
169
- <img src={iconSrc} width="96" height="96" alt="" className="mb-3" />
170
- {NAV_ITEMS.map(({ id, href, label, icon: Icon }) => (
171
- <a
172
- key={id}
173
- href={href}
174
- aria-label={label}
175
- aria-current={id === active ? "page" : undefined}
176
- title={label}
177
- className={[
178
- "w-10 h-10 flex items-center justify-center rounded-sm",
179
- id === active
180
- ? "bg-primary/10 text-primary-dark"
181
- : "text-text-muted hover:bg-surface-alt hover:text-text",
182
- ].join(" ")}
183
- >
184
- <Icon size={20} aria-hidden="true" />
185
- </a>
186
- ))}
187
- </nav>
188
- <BottomTabBar apps={NAV_ITEMS} active={active} />
189
- <div className="flex-1 flex flex-col min-w-0">
190
- <header className="h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6">
191
- <span className="font-display font-bold text-lg uppercase tracking-wide">{activeItem?.label}</span>
192
- <UserMenu userUid={userUid} authServerUrl={authServerUrl} onSignOut={handleSignOut} />
193
- </header>
194
- <div className="flex-1 pb-14 md:pb-0">
195
- <main className="max-w-6xl mx-auto px-6 py-8">{children}</main>
196
- </div>
197
- </div>
198
- </div>
199
- </div>
200
- );
201
- }
202
-
203
- return (
204
- <>
205
- <BrandingHeader branding={branding} />
206
- {content}
207
- <BrandingFooter branding={branding} />
208
- </>
209
- );
210
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import "../../../styles/app.css";
6
+ import React, { PropsWithChildren, ReactNode, useEffect, useState } from "react";
7
+ import {
8
+ HiOutlineClipboardDocumentList,
9
+ HiOutlineClock,
10
+ HiOutlineDocumentArrowDown,
11
+ HiOutlineGlobeAlt,
12
+ HiOutlineInboxStack,
13
+ HiOutlineKey,
14
+ HiOutlineLockClosed,
15
+ HiOutlinePaintBrush,
16
+ HiOutlinePuzzlePiece,
17
+ HiOutlineQueueList,
18
+ HiOutlineRocketLaunch,
19
+ HiOutlineShieldCheck,
20
+ HiOutlineShieldExclamation,
21
+ HiOutlineUserGroup,
22
+ HiOutlineWrenchScrewdriver,
23
+ } from "react-icons/hi2";
24
+ import { apiFetch, ApiRequestError } from "@rapidmx/react-shared/util/api.js";
25
+ import { getSetupStatus } from "@rapidmx/react-shared/admin/setupApi.js";
26
+ import { useRedirectIfUnauthenticated } from "@rapidmx/react-shared/auth/session.js";
27
+ import useBranding from "@rapidmx/react-shared/branding/useBranding.js";
28
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
29
+ import BottomTabBar, { NavItem } from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
30
+ import { BrandingFooter, BrandingHeader } from "../../layout/BrandingChrome.js";
31
+ import UserMenu from "../../layout/UserMenu.js";
32
+ import { signOutOfConsole } from "../signOut.js";
33
+
34
+ export type AdminSection =
35
+ | "mailboxes"
36
+ | "quarantine"
37
+ | "ingestQueue"
38
+ | "domains"
39
+ | "auditLog"
40
+ | "distributionLists"
41
+ | "transportRules"
42
+ | "escrowScopes"
43
+ | "retentionPolicy"
44
+ | "encryptionPolicy"
45
+ | "mailboxPolicy"
46
+ | "setup"
47
+ | "dataRequests"
48
+ | "plugins"
49
+ | "branding";
50
+
51
+ export interface AdminShellProps {
52
+ /** Which icon in the rail is highlighted as the current section. */
53
+ active: AdminSection;
54
+ /** Populated automatically by the framework from an authenticated request (e.g. a valid `jwt` cookie). */
55
+ userUid?: string;
56
+ /** auth-server's base URL, injected via the route's `fetchProps` — see `src/mongo/routes/AdminConsoleRoute.ts`. */
57
+ authServerUrl?: string;
58
+ /**
59
+ * Where `mailApi.ts`'s `impersonateUser()`/`stopImpersonating()` should call: the real auth-server in
60
+ * production, or `""` under `yarn dev` to call this app's own local dev-only impersonation endpoint
61
+ * instead — see `AdminConsoleRoute`'s `fetchProps` and `src/dev/DevImpersonationRoute.ts`.
62
+ */
63
+ impersonationBaseUrl?: string;
64
+ }
65
+
66
+ type Status = "checking" | "denied" | "error" | "authorized";
67
+
68
+ /** Sections shown in the persistent icon rail / mobile tab bar — every admin area reachable from
69
+ * anywhere in the console. Deliberately excludes `quarantine`/`ingestQueue`: those are scoped to a
70
+ * single mailbox and only ever reached via links on that mailbox's own detail page (see
71
+ * `apps/admin/mailboxes/[uid].tsx`), not global navigation destinations. */
72
+ const NAV_ITEMS: NavItem[] = [
73
+ { id: "mailboxes", href: "/admin", label: "Mailboxes", icon: HiOutlineInboxStack },
74
+ { id: "domains", href: "/admin/domains", label: "Domains", icon: HiOutlineGlobeAlt },
75
+ { id: "auditLog", href: "/admin/audit-log", label: "Audit Log", icon: HiOutlineClipboardDocumentList },
76
+ {
77
+ id: "distributionLists",
78
+ href: "/admin/distribution-lists",
79
+ label: "Distribution Lists",
80
+ icon: HiOutlineUserGroup,
81
+ },
82
+ {
83
+ id: "transportRules",
84
+ href: "/admin/transport-rules",
85
+ label: "Transport Rules",
86
+ icon: HiOutlineShieldCheck,
87
+ },
88
+ {
89
+ id: "escrowScopes",
90
+ href: "/admin/escrow-scopes",
91
+ label: "Escrow Scopes",
92
+ icon: HiOutlineKey,
93
+ },
94
+ {
95
+ id: "encryptionPolicy",
96
+ href: "/admin/encryption-policy",
97
+ label: "Encryption Policy",
98
+ icon: HiOutlineLockClosed,
99
+ },
100
+ {
101
+ id: "mailboxPolicy",
102
+ href: "/admin/mailbox-policy",
103
+ label: "Mailbox Policy",
104
+ icon: HiOutlineWrenchScrewdriver,
105
+ },
106
+ {
107
+ id: "retentionPolicy",
108
+ href: "/admin/retention-policy",
109
+ label: "Retention Policy",
110
+ icon: HiOutlineClock,
111
+ },
112
+ {
113
+ id: "dataRequests",
114
+ href: "/admin/data-requests",
115
+ label: "Data Requests",
116
+ icon: HiOutlineDocumentArrowDown,
117
+ },
118
+ { id: "plugins", href: "/admin/plugins", label: "Plugins", icon: HiOutlinePuzzlePiece },
119
+ { id: "branding", href: "/admin/branding", label: "Branding", icon: HiOutlinePaintBrush },
120
+ ];
121
+
122
+ /** Mailbox-scoped sections — not part of the global nav rail/tab bar (see `NAV_ITEMS` above), but
123
+ * still need an entry here so the header can resolve a label for them when they're the active page. */
124
+ const MAILBOX_SCOPED_ITEMS: NavItem[] = [
125
+ { id: "quarantine", href: "/admin/quarantine", label: "Quarantine", icon: HiOutlineShieldExclamation },
126
+ { id: "ingestQueue", href: "/admin/ingest-queue", label: "Ingest Queue", icon: HiOutlineQueueList },
127
+ ];
128
+
129
+ /** The setup wizard - reached by redirect or from the Mailboxes page, not from the rail. */
130
+ const SETUP_ITEM: NavItem = { id: "setup", href: "/admin/setup", label: "Setup", icon: HiOutlineRocketLaunch };
131
+
132
+ const ALL_ITEMS: NavItem[] = [...NAV_ITEMS, ...MAILBOX_SCOPED_ITEMS, SETUP_ITEM];
133
+
134
+ /**
135
+ * Gates every `apps/admin` page behind the `admin` trusted role. Uses `GET /api/admin/release-notes` (any
136
+ * `BaseAdminRoute` endpoint works — this one is side-effect-free) purely as a canary: a 200 means the
137
+ * caller's JWT carries a trusted role, a 403 means it doesn't. There is no local step-up/elevation flow
138
+ * (that would need a cross-origin call to auth-server's own elevation endpoint — not wired up yet).
139
+ */
140
+ export default function AdminShell({ active, userUid, authServerUrl, children }: PropsWithChildren<AdminShellProps>) {
141
+ const [status, setStatus] = useState<Status>("checking");
142
+ const [error, setError] = useState<string | null>(null);
143
+ const { branding, iconSrc } = useBranding();
144
+
145
+ useRedirectIfUnauthenticated(userUid, authServerUrl);
146
+
147
+ useEffect(() => {
148
+ if (!userUid) {
149
+ return;
150
+ }
151
+ apiFetch("/admin/release-notes")
152
+ .then(async () => {
153
+ // Until first-run setup is finished, every other admin page sends the admin to the wizard. A failed
154
+ // check never blocks the console - the admin can still reach setup from the Mailboxes page.
155
+ if (active !== "setup") {
156
+ try {
157
+ if ((await getSetupStatus()).required) {
158
+ window.location.href = SETUP_ITEM.href;
159
+ return;
160
+ }
161
+ } catch {
162
+ // Fall through to the page.
163
+ }
164
+ }
165
+ setStatus("authorized");
166
+ })
167
+ .catch((err) => {
168
+ if (err instanceof ApiRequestError && (err.status === 403 || err.status === 401)) {
169
+ setStatus("denied");
170
+ return;
171
+ }
172
+ setError(err instanceof ApiRequestError ? err.message : "Could not verify administrator access.");
173
+ setStatus("error");
174
+ });
175
+ }, [userUid]);
176
+
177
+ function handleSignOut() {
178
+ // Ends the auth-server session and tells other tabs, not just navigates - see `signOutOfConsole()`.
179
+ void signOutOfConsole(authServerUrl);
180
+ }
181
+
182
+ let content: ReactNode;
183
+ if (!userUid || status === "checking") {
184
+ content = <div className="min-h-screen" />;
185
+ } else if (status === "denied") {
186
+ content = (
187
+ <div className="min-h-screen flex items-center justify-center p-8">
188
+ <div className="w-full max-w-md">
189
+ <Alert>You do not have administrator access.</Alert>
190
+ </div>
191
+ </div>
192
+ );
193
+ } else if (status === "error") {
194
+ content = (
195
+ <div className="min-h-screen flex items-center justify-center p-8">
196
+ <div className="w-full max-w-md">
197
+ <Alert>{error}</Alert>
198
+ </div>
199
+ </div>
200
+ );
201
+ } else {
202
+ const activeItem = ALL_ITEMS.find((item) => item.id === active);
203
+ content = (
204
+ <div className="min-h-screen flex flex-col bg-surface-alt">
205
+ <div className="flex-1 flex min-h-0">
206
+ <nav
207
+ aria-label="Admin sections"
208
+ className="hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center py-3 gap-1"
209
+ >
210
+ <img src={iconSrc} width="96" height="96" alt="" className="mb-3" />
211
+ {NAV_ITEMS.map(({ id, href, label, icon: Icon }) => (
212
+ <a
213
+ key={id}
214
+ href={href}
215
+ aria-label={label}
216
+ aria-current={id === active ? "page" : undefined}
217
+ title={label}
218
+ className={[
219
+ "w-10 h-10 flex items-center justify-center rounded-sm",
220
+ id === active
221
+ ? "bg-primary/10 text-primary-dark"
222
+ : "text-text-muted hover:bg-surface-alt hover:text-text",
223
+ ].join(" ")}
224
+ >
225
+ <Icon size={20} aria-hidden="true" />
226
+ </a>
227
+ ))}
228
+ </nav>
229
+ <BottomTabBar apps={NAV_ITEMS} active={active} />
230
+ <div className="flex-1 flex flex-col min-w-0">
231
+ <header className="h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6">
232
+ <span className="font-display font-bold text-lg uppercase tracking-wide">{activeItem?.label}</span>
233
+ <UserMenu userUid={userUid} authServerUrl={authServerUrl} onSignOut={handleSignOut} />
234
+ </header>
235
+ <div className="flex-1 pb-14 md:pb-0">
236
+ <main className="max-w-6xl mx-auto px-6 py-8">{children}</main>
237
+ </div>
238
+ </div>
239
+ </div>
240
+ </div>
241
+ );
242
+ }
243
+
244
+ return (
245
+ <>
246
+ <BrandingHeader branding={branding} />
247
+ {content}
248
+ <BrandingFooter branding={branding} />
249
+ </>
250
+ );
251
+ }