@rapidmx/web-client 0.4.0 → 0.6.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.
- package/README.md +88 -0
- package/apps/admin/audit-log/index.tsx +35 -8
- package/apps/admin/data-requests/index.tsx +480 -485
- package/apps/admin/distribution-lists/[uid].tsx +19 -1
- package/apps/admin/escrow-scopes/[uid].tsx +348 -237
- package/apps/admin/escrow-scopes/new/index.tsx +8 -2
- package/apps/admin/index.tsx +19 -1
- package/apps/admin/ingest-queue/index.tsx +44 -5
- package/apps/admin/mailboxes/[uid].tsx +211 -184
- package/apps/admin/quarantine/index.tsx +46 -14
- package/apps/admin/transport-rules/[uid].tsx +30 -1
- package/apps/admin/transport-rules/_transportRuleConfig.tsx +38 -2
- package/apps/admin/transport-rules/index.tsx +24 -6
- package/apps/admin/transport-rules/new/index.tsx +30 -1
- package/apps/escrow/_layout.tsx +2 -3
- package/apps/escrow/audit-log/index.tsx +196 -168
- package/apps/escrow/matters/[uid].tsx +621 -521
- package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +22 -1
- package/apps/shared/components/admin/layout/AdminShell.tsx +28 -9
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -0
- package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
- package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +55 -11
- package/apps/shared/components/admin/settings/BrandingForm.tsx +15 -4
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +18 -7
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +10 -2
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +6 -0
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +45 -12
- package/apps/shared/components/admin/settings/PluginsManager.tsx +1235 -595
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +174 -98
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +49 -13
- package/apps/shared/components/admin/setup/SetupWizard.tsx +150 -34
- package/apps/shared/components/admin/signOut.ts +79 -0
- package/apps/shared/components/admin/usePagedList.tsx +129 -0
- package/apps/shared/components/calendar/EventModal.tsx +717 -549
- package/apps/shared/components/calendar/MonthView.tsx +173 -139
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +222 -180
- package/apps/shared/components/calendar/SplitDayView.tsx +143 -137
- package/apps/shared/components/calendar/TimeGridView.tsx +244 -198
- package/apps/shared/components/calendar/allDay.ts +124 -0
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +194 -192
- package/apps/shared/components/contacts/ContactDetailPane.tsx +100 -21
- package/apps/shared/components/contacts/ContactForm.tsx +377 -348
- package/apps/shared/components/contacts/ContactsSidebar.tsx +6 -2
- package/apps/shared/components/contacts/KeyChangeReview.tsx +192 -0
- package/apps/shared/components/contacts/contactKeys.ts +78 -0
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +199 -197
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +148 -134
- package/apps/shared/components/layout/AppShell.tsx +123 -33
- package/apps/shared/components/layout/BrandingChrome.tsx +48 -10
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +389 -298
- package/apps/shared/components/layout/MailboxProvisioning.tsx +19 -3
- package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +350 -0
- package/apps/shared/components/layout/UnlockPromptProvider.tsx +257 -128
- package/apps/shared/components/mail/ConversationThreadPane.tsx +216 -181
- package/apps/shared/components/mail/MessageDetailPane.tsx +1318 -630
- package/apps/shared/components/mail/compose/ComposeContext.tsx +20 -15
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +854 -132
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +420 -408
- package/apps/shared/components/mail/pinnedSigners.ts +124 -0
- package/apps/shared/components/mail/verificationSeals.ts +125 -0
- package/apps/shared/components/mail/writableMailboxes.ts +101 -0
- package/apps/shared/components/rules/RuleBuilder.tsx +311 -276
- package/apps/shared/components/settings/layout/SettingsShell.tsx +23 -9
- package/apps/shared/components/tasks/layout/TasksShell.tsx +201 -199
- package/apps/shared/mail/listAllPages.ts +39 -0
- package/apps/shared/plugins/pluginNav.ts +73 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +64 -55
- package/apps/shared/search/localIndexBuilder.ts +481 -179
- package/apps/shared/search/localIndexRpcClient.ts +231 -29
- package/apps/shared/search/localIndexSchema.ts +15 -6
- package/apps/shared/search/localIndexStorage.ts +98 -0
- package/apps/shared/search/localIndexVFS.ts +32 -1
- package/apps/shared/search/localIndexWorker.ts +648 -186
- package/apps/www/calendar/index.tsx +438 -417
- package/apps/www/contacts/[uid].tsx +21 -2
- package/apps/www/contacts/index.tsx +116 -12
- package/apps/www/index.tsx +1314 -1041
- package/apps/www/messages/[uid].tsx +18 -6
- package/apps/www/settings/auto-reply/index.tsx +134 -129
- package/apps/www/settings/encryption/index.tsx +739 -216
- package/apps/www/settings/filters/[uid].tsx +171 -155
- package/apps/www/settings/filters/new/index.tsx +8 -1
- package/apps/www/settings/focused-inbox/index.tsx +150 -148
- package/apps/www/settings/privacy/index.tsx +494 -436
- package/apps/www/settings/read-receipts/index.tsx +148 -125
- package/apps/www/settings/sharing/index.tsx +17 -14
- package/apps/www/tasks/index.tsx +638 -579
- package/dist/apps/admin/_404.d.ts +2 -0
- package/dist/apps/admin/_500.d.ts +2 -0
- package/dist/apps/admin/_layout.d.ts +8 -0
- package/dist/apps/admin/audit-log/index.d.ts +11 -0
- package/dist/apps/admin/audit-log/index.js +38 -8
- package/dist/apps/admin/branding/index.d.ts +3 -0
- package/dist/apps/admin/data-requests/index.d.ts +3 -0
- package/dist/apps/admin/data-requests/index.js +52 -72
- package/dist/apps/admin/distribution-lists/[uid].d.ts +7 -0
- package/dist/apps/admin/distribution-lists/[uid].js +1 -1
- package/dist/apps/admin/distribution-lists/index.d.ts +3 -0
- package/dist/apps/admin/distribution-lists/new/index.d.ts +3 -0
- package/dist/apps/admin/domains/[uid].d.ts +7 -0
- package/dist/apps/admin/domains/index.d.ts +3 -0
- package/dist/apps/admin/domains/new/index.d.ts +3 -0
- package/dist/apps/admin/encryption-policy/index.d.ts +3 -0
- package/dist/apps/admin/escrow-scopes/[uid].d.ts +21 -0
- package/dist/apps/admin/escrow-scopes/[uid].js +91 -12
- package/dist/apps/admin/escrow-scopes/index.d.ts +3 -0
- package/dist/apps/admin/escrow-scopes/new/index.d.ts +3 -0
- package/dist/apps/admin/escrow-scopes/new/index.js +8 -3
- package/dist/apps/admin/index.d.ts +3 -0
- package/dist/apps/admin/index.js +4 -1
- package/dist/apps/admin/ingest-queue/index.d.ts +4 -0
- package/dist/apps/admin/ingest-queue/index.js +24 -6
- package/dist/apps/admin/mailbox-policy/index.d.ts +3 -0
- package/dist/apps/admin/mailboxes/[uid].d.ts +7 -0
- package/dist/apps/admin/mailboxes/[uid].js +20 -4
- package/dist/apps/admin/mailboxes/new/index.d.ts +3 -0
- package/dist/apps/admin/plugins/index.d.ts +3 -0
- package/dist/apps/admin/quarantine/index.d.ts +6 -0
- package/dist/apps/admin/quarantine/index.js +20 -11
- package/dist/apps/admin/retention-policy/index.d.ts +3 -0
- package/dist/apps/admin/setup/index.d.ts +3 -0
- package/dist/apps/admin/transport-rules/[uid].d.ts +7 -0
- package/dist/apps/admin/transport-rules/[uid].js +18 -2
- package/dist/apps/admin/transport-rules/_transportRuleConfig.d.ts +21 -0
- package/dist/apps/admin/transport-rules/_transportRuleConfig.js +19 -0
- package/dist/apps/admin/transport-rules/index.d.ts +3 -0
- package/dist/apps/admin/transport-rules/index.js +22 -6
- package/dist/apps/admin/transport-rules/new/index.d.ts +3 -0
- package/dist/apps/admin/transport-rules/new/index.js +18 -2
- package/dist/apps/escrow/_layout.d.ts +10 -0
- package/dist/apps/escrow/_layout.js +3 -3
- package/dist/apps/escrow/audit-log/index.d.ts +9 -0
- package/dist/apps/escrow/audit-log/index.js +26 -1
- package/dist/apps/escrow/index.d.ts +3 -0
- package/dist/apps/escrow/matters/[uid].d.ts +7 -0
- package/dist/apps/escrow/matters/[uid].js +77 -42
- package/dist/apps/escrow/matters/new/index.d.ts +3 -0
- package/dist/apps/shared/components/admin/distributionLists/MemberListCard.d.ts +17 -0
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +32 -0
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +11 -2
- package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +32 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.js +15 -6
- package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.d.ts +12 -0
- package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +72 -0
- package/dist/apps/shared/components/admin/mailboxes/MailboxTable.d.ts +6 -0
- package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.d.ts +19 -0
- package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +12 -0
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +22 -4
- package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +9 -0
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +15 -4
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.d.ts +12 -0
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +13 -7
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.d.ts +12 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +7 -2
- package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.d.ts +9 -0
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.d.ts +19 -0
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +6 -0
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.d.ts +10 -0
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +34 -12
- package/dist/apps/shared/components/admin/settings/PluginsManager.d.ts +4 -0
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +390 -78
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.d.ts +9 -0
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +40 -11
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.d.ts +14 -0
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +29 -13
- package/dist/apps/shared/components/admin/setup/SetupWizard.d.ts +36 -0
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +87 -21
- package/dist/apps/shared/components/admin/signOut.d.ts +23 -0
- package/dist/apps/shared/components/admin/signOut.js +75 -0
- package/dist/apps/shared/components/admin/usePagedList.d.ts +39 -0
- package/dist/apps/shared/components/admin/usePagedList.js +99 -0
- package/dist/apps/shared/components/calendar/CalendarListSidebar.d.ts +26 -0
- package/dist/apps/shared/components/calendar/EventModal.d.ts +46 -0
- package/dist/apps/shared/components/calendar/EventModal.js +142 -21
- package/dist/apps/shared/components/calendar/MonthView.d.ts +15 -0
- package/dist/apps/shared/components/calendar/MonthView.js +12 -5
- package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +20 -0
- package/dist/apps/shared/components/calendar/RecurrenceEditor.js +40 -5
- package/dist/apps/shared/components/calendar/ResourcePicker.d.ts +19 -0
- package/dist/apps/shared/components/calendar/SplitDayView.d.ts +28 -0
- package/dist/apps/shared/components/calendar/SplitDayView.js +10 -4
- package/dist/apps/shared/components/calendar/TimeGridView.d.ts +17 -0
- package/dist/apps/shared/components/calendar/TimeGridView.js +23 -7
- package/dist/apps/shared/components/calendar/allDay.d.ts +51 -0
- package/dist/apps/shared/components/calendar/allDay.js +111 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +40 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +3 -3
- package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +23 -0
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +43 -5
- package/dist/apps/shared/components/contacts/ContactForm.d.ts +20 -0
- package/dist/apps/shared/components/contacts/ContactForm.js +32 -10
- package/dist/apps/shared/components/contacts/ContactsSidebar.d.ts +40 -0
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +2 -2
- package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +25 -0
- package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +40 -0
- package/dist/apps/shared/components/contacts/KeyChangeReview.js +63 -0
- package/dist/apps/shared/components/contacts/contactKeys.d.ts +29 -0
- package/dist/apps/shared/components/contacts/contactKeys.js +64 -0
- package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +21 -0
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +2 -2
- package/dist/apps/shared/components/escrow/layout/EscrowShell.d.ts +25 -0
- package/dist/apps/shared/components/escrow/layout/EscrowShell.js +24 -5
- package/dist/apps/shared/components/forms/StringListField.d.ts +22 -0
- package/dist/apps/shared/components/layout/AppShell.d.ts +49 -0
- package/dist/apps/shared/components/layout/AppShell.js +97 -26
- package/dist/apps/shared/components/layout/BrandingChrome.d.ts +18 -0
- package/dist/apps/shared/components/layout/BrandingChrome.js +45 -10
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.d.ts +51 -0
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +49 -12
- package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +13 -0
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +10 -2
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +66 -0
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +174 -0
- package/dist/apps/shared/components/layout/UnlockPromptProvider.d.ts +45 -0
- package/dist/apps/shared/components/layout/UnlockPromptProvider.js +73 -14
- package/dist/apps/shared/components/layout/UserMenu.d.ts +23 -0
- package/dist/apps/shared/components/mail/ConversationList.d.ts +11 -0
- package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +23 -0
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +50 -12
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +93 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.js +441 -36
- package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +58 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +9 -6
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +15 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +37 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +699 -128
- package/dist/apps/shared/components/mail/compose/EmojiPicker.d.ts +14 -0
- package/dist/apps/shared/components/mail/compose/GifPicker.d.ts +13 -0
- package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +34 -0
- package/dist/apps/shared/components/mail/compose/ScheduleSendPicker.d.ts +16 -0
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +27 -0
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +50 -0
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +57 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +13 -4
- package/dist/apps/shared/components/mail/pinnedSigners.d.ts +33 -0
- package/dist/apps/shared/components/mail/pinnedSigners.js +98 -0
- package/dist/apps/shared/components/mail/verificationSeals.d.ts +22 -0
- package/dist/apps/shared/components/mail/verificationSeals.js +112 -0
- package/dist/apps/shared/components/mail/writableMailboxes.d.ts +15 -0
- package/dist/apps/shared/components/mail/writableMailboxes.js +87 -0
- package/dist/apps/shared/components/rules/RuleBuilder.d.ts +70 -0
- package/dist/apps/shared/components/rules/RuleBuilder.js +34 -6
- package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +41 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +19 -9
- package/dist/apps/shared/components/tasks/TasksSidebar.d.ts +41 -0
- package/dist/apps/shared/components/tasks/TasksToolbar.d.ts +14 -0
- package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +24 -0
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +2 -2
- package/dist/apps/shared/mail/findWellKnownFolderUid.d.ts +5 -0
- package/dist/apps/shared/mail/listAllPages.d.ts +17 -0
- package/dist/apps/shared/mail/listAllPages.js +25 -0
- package/dist/apps/shared/plugins/pluginNav.d.ts +39 -0
- package/dist/apps/shared/plugins/pluginNav.js +33 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.d.ts +13 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.js +58 -54
- package/dist/apps/shared/search/localIndexBlockCipher.d.ts +36 -0
- package/dist/apps/shared/search/localIndexBuilder.d.ts +85 -0
- package/dist/apps/shared/search/localIndexBuilder.js +292 -37
- package/dist/apps/shared/search/localIndexKey.d.ts +4 -0
- package/dist/apps/shared/search/localIndexRpcClient.d.ts +71 -0
- package/dist/apps/shared/search/localIndexRpcClient.js +191 -26
- package/dist/apps/shared/search/localIndexSchema.d.ts +111 -0
- package/dist/apps/shared/search/localIndexSchema.js +12 -6
- package/dist/apps/shared/search/localIndexSizePreference.d.ts +27 -0
- package/dist/apps/shared/search/localIndexStorage.d.ts +28 -0
- package/dist/apps/shared/search/localIndexStorage.js +86 -0
- package/dist/apps/shared/search/localIndexVFS.d.ts +101 -0
- package/dist/apps/shared/search/localIndexVFS.js +68 -40
- package/dist/apps/shared/search/localIndexWorker.d.ts +145 -0
- package/dist/apps/shared/search/localIndexWorker.js +519 -173
- package/dist/apps/shared/search/searchTier2.d.ts +30 -0
- package/dist/apps/www/_404.d.ts +2 -0
- package/dist/apps/www/_500.d.ts +2 -0
- package/dist/apps/www/_layout.d.ts +14 -0
- package/dist/apps/www/calendar/index.d.ts +3 -0
- package/dist/apps/www/calendar/index.js +28 -10
- package/dist/apps/www/contacts/[uid].d.ts +13 -0
- package/dist/apps/www/contacts/[uid].js +13 -2
- package/dist/apps/www/contacts/index.d.ts +3 -0
- package/dist/apps/www/contacts/index.js +80 -10
- package/dist/apps/www/index.d.ts +3 -0
- package/dist/apps/www/index.js +339 -108
- package/dist/apps/www/messages/[uid].d.ts +11 -0
- package/dist/apps/www/messages/[uid].js +18 -6
- package/dist/apps/www/settings/auto-reply/index.d.ts +4 -0
- package/dist/apps/www/settings/auto-reply/index.js +9 -4
- package/dist/apps/www/settings/encryption/index.d.ts +7 -0
- package/dist/apps/www/settings/encryption/index.js +514 -97
- package/dist/apps/www/settings/filters/[uid].d.ts +8 -0
- package/dist/apps/www/settings/filters/[uid].js +22 -7
- package/dist/apps/www/settings/filters/_mailFilterRuleConfig.d.ts +11 -0
- package/dist/apps/www/settings/filters/index.d.ts +4 -0
- package/dist/apps/www/settings/filters/new/index.d.ts +4 -0
- package/dist/apps/www/settings/filters/new/index.js +8 -2
- package/dist/apps/www/settings/focused-inbox/index.d.ts +4 -0
- package/dist/apps/www/settings/focused-inbox/index.js +3 -1
- package/dist/apps/www/settings/labels/index.d.ts +4 -0
- package/dist/apps/www/settings/privacy/index.d.ts +4 -0
- package/dist/apps/www/settings/privacy/index.js +57 -22
- package/dist/apps/www/settings/read-receipts/index.d.ts +4 -0
- package/dist/apps/www/settings/read-receipts/index.js +10 -3
- package/dist/apps/www/settings/sharing/index.d.ts +4 -0
- package/dist/apps/www/settings/sharing/index.js +7 -11
- package/dist/apps/www/settings/signatures/[uid].d.ts +8 -0
- package/dist/apps/www/settings/signatures/index.d.ts +4 -0
- package/dist/apps/www/settings/signatures/new/index.d.ts +4 -0
- package/dist/apps/www/settings/signatures/signatureDefaults.d.ts +10 -0
- package/dist/apps/www/tasks/index.d.ts +3 -0
- package/dist/apps/www/tasks/index.js +50 -10
- package/package.json +3 -3
- package/apps/shared/components/booking/AvailabilityEditor.tsx +0 -118
- package/apps/www/settings/booking-types/[uid].tsx +0 -300
- package/apps/www/settings/booking-types/index.tsx +0 -98
- package/apps/www/settings/booking-types/new/index.tsx +0 -212
- package/dist/apps/shared/components/booking/AvailabilityEditor.js +0 -45
- package/dist/apps/www/settings/booking-types/[uid].js +0 -129
- package/dist/apps/www/settings/booking-types/index.js +0 -29
- package/dist/apps/www/settings/booking-types/new/index.js +0 -86
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import "../../styles/app.css";
|
|
2
|
+
import React, { PropsWithChildren } from "react";
|
|
3
|
+
import type { IconType } from "react-icons";
|
|
4
|
+
import { NavItem } from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
|
|
5
|
+
import { PluginNav, PluginNavProps } from "../../plugins/pluginNav.js";
|
|
6
|
+
/** How long sign-out waits for auth-server's logout before navigating anyway. */
|
|
7
|
+
export declare const LOGOUT_TIMEOUT_MS = 3000;
|
|
8
|
+
export type AppShellApp = "mail" | "calendar" | "contacts" | "tasks";
|
|
9
|
+
/** `"settings"` is a valid `active` value but deliberately has no entry in `APPS` below — Settings is
|
|
10
|
+
* reached via a `UserMenu` item, not a 5th rail icon (see `SettingsShell.tsx`), so it highlights no
|
|
11
|
+
* rail/tab icon at all; only the header title needs to account for it. Any other string is a plugin's
|
|
12
|
+
* `appRail` item id (see `PluginNav`). */
|
|
13
|
+
export type AppShellActive = AppShellApp | "settings" | (string & {});
|
|
14
|
+
export interface AppShellProps extends PluginNavProps {
|
|
15
|
+
/** Which icon in the rail is highlighted as the current app — `"settings"` highlights none, and a plugin
|
|
16
|
+
* app page passes its own `appRail` item id. */
|
|
17
|
+
active: AppShellActive;
|
|
18
|
+
/** Populated automatically by the framework from an authenticated request (e.g. a valid `jwt` cookie). */
|
|
19
|
+
userUid?: string;
|
|
20
|
+
/** auth-server's base URL, injected via the route's `fetchProps`. */
|
|
21
|
+
authServerUrl?: string;
|
|
22
|
+
/**
|
|
23
|
+
* `true` when this session is an admin "log in as user" impersonation (a `jwt_impersonator` cookie is
|
|
24
|
+
* present) — see `MailShell`'s original doc comment, unchanged now that this lives here. Drives the
|
|
25
|
+
* "you are viewing as this user — stop impersonating" banner below.
|
|
26
|
+
*/
|
|
27
|
+
impersonating?: boolean;
|
|
28
|
+
/** Where `mailApi.ts`'s `stopImpersonating()` should call — see `MailShell`'s original doc comment. */
|
|
29
|
+
impersonationBaseUrl?: string;
|
|
30
|
+
/** `true` when the caller's JWT carries a trusted role — shows an "Admin" item in the user menu below. */
|
|
31
|
+
trusted?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface AppDef {
|
|
34
|
+
id: AppShellApp;
|
|
35
|
+
href: string;
|
|
36
|
+
label: string;
|
|
37
|
+
icon: IconType;
|
|
38
|
+
}
|
|
39
|
+
export declare const APPS: AppDef[];
|
|
40
|
+
/** `APPS` followed by the plugins' `appRail` items (generic icon), core ids winning - see `mergePluginNavItems`. */
|
|
41
|
+
export declare function appRailItems(pluginNav?: PluginNav): NavItem[];
|
|
42
|
+
/**
|
|
43
|
+
* The persistent chrome shared by every webmail app (Mail, Calendar, Contacts, Tasks): a left icon rail for
|
|
44
|
+
* switching apps (full page loads — see `MailShell`'s original doc comment on this framework having no
|
|
45
|
+
* client-side router), a header with the current app's name and `UserMenu`, and the impersonation banner.
|
|
46
|
+
* Each app's own shell (e.g. `MailShell`) renders its own contextual sidebar + content as `children`, inside
|
|
47
|
+
* the area to the right of the icon rail and below the header.
|
|
48
|
+
*/
|
|
49
|
+
export default function AppShell({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }: PropsWithChildren<AppShellProps>): React.JSX.Element;
|
|
@@ -4,34 +4,60 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import "../../styles/app.css";
|
|
7
|
-
import {
|
|
8
|
-
import { HiOutlineCalendarDays, HiOutlineClipboardDocumentList, HiOutlineEnvelope, HiOutlineUsers } from "react-icons/hi2";
|
|
7
|
+
import { useEffect, useRef, useState } from "react";
|
|
8
|
+
import { HiOutlineCalendarDays, HiOutlineClipboardDocumentList, HiOutlineEnvelope, HiOutlinePuzzlePiece, HiOutlineUsers, } from "react-icons/hi2";
|
|
9
9
|
import { useRedirectIfUnauthenticated } from "@rapidmx/react-shared/auth/session.js";
|
|
10
10
|
import { getSetupStatus } from "@rapidmx/react-shared/admin/setupApi.js";
|
|
11
11
|
import { stopImpersonating } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
12
12
|
import useBranding from "@rapidmx/react-shared/branding/useBranding.js";
|
|
13
13
|
import { useIdleKeyTimeout } from "@rapidmx/react-shared/crypto/useIdleKeyTimeout.js";
|
|
14
14
|
import ComposeProvider from "../mail/compose/ComposeContext.js";
|
|
15
|
+
import { flushComposeDrafts, markSigningOut } from "../mail/compose/composeFlushRegistry.js";
|
|
15
16
|
import BottomTabBar from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
|
|
16
17
|
import { BrandingFooter, BrandingHeader } from "./BrandingChrome.js";
|
|
17
18
|
import UserMenu from "./UserMenu.js";
|
|
18
19
|
import { UnlockPromptProvider } from "./UnlockPromptProvider.js";
|
|
19
|
-
import { destroyAllLocalIndexes } from "../../search/localIndexRpcClient.js";
|
|
20
|
+
import { SIGN_OUT_CHANNEL, destroyAllLocalIndexes } from "../../search/localIndexRpcClient.js";
|
|
21
|
+
import { authApiFetch } from "@rapidmx/react-shared/util/api.js";
|
|
22
|
+
import { destroyUnlockedKeys } from "@rapidmx/react-shared/crypto/keySession.js";
|
|
23
|
+
import { clearPinnedSignerCache } from "../mail/pinnedSigners.js";
|
|
24
|
+
import { mergePluginNavItems } from "../../plugins/pluginNav.js";
|
|
25
|
+
/** How long sign-out waits for auth-server's logout before navigating anyway. */
|
|
26
|
+
export const LOGOUT_TIMEOUT_MS = 3000;
|
|
27
|
+
/**
|
|
28
|
+
* Calls auth-server's logout (`POST /api/auth/logout`, cross-origin with credentials), which clears the auth
|
|
29
|
+
* cookie and invalidates the session's refresh token. Bounded by `LOGOUT_TIMEOUT_MS` and never rejects - a
|
|
30
|
+
* failure (unreachable server, CORS) must not keep the user from leaving.
|
|
31
|
+
*/
|
|
32
|
+
async function logOutOfAuthServer(authServerUrl) {
|
|
33
|
+
if (!authServerUrl) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const controller = new AbortController();
|
|
37
|
+
const timer = setTimeout(() => controller.abort(), LOGOUT_TIMEOUT_MS);
|
|
38
|
+
try {
|
|
39
|
+
await authApiFetch(authServerUrl, "/auth/logout", { method: "POST", signal: controller.signal });
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Navigate anyway - see this function's doc comment.
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
20
48
|
export const APPS = [
|
|
21
49
|
{ id: "mail", href: "/", label: "Mail", icon: HiOutlineEnvelope },
|
|
22
50
|
{ id: "calendar", href: "/calendar", label: "Calendar", icon: HiOutlineCalendarDays },
|
|
23
51
|
{ id: "contacts", href: "/contacts", label: "Contacts", icon: HiOutlineUsers },
|
|
24
52
|
{ id: "tasks", href: "/tasks", label: "Tasks", icon: HiOutlineClipboardDocumentList },
|
|
25
53
|
];
|
|
26
|
-
/**
|
|
27
|
-
*
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
settings: "Settings",
|
|
34
|
-
};
|
|
54
|
+
/** Ids plugin `appRail` items can't take besides `APPS`' own - `"settings"` has no rail icon but is still a
|
|
55
|
+
* core `active` value. */
|
|
56
|
+
const RESERVED_APP_IDS = ["settings"];
|
|
57
|
+
/** `APPS` followed by the plugins' `appRail` items (generic icon), core ids winning - see `mergePluginNavItems`. */
|
|
58
|
+
export function appRailItems(pluginNav) {
|
|
59
|
+
return mergePluginNavItems(APPS, pluginNav?.appRail, ({ id, href, label }) => ({ id, href, label, icon: HiOutlinePuzzlePiece }), RESERVED_APP_IDS);
|
|
60
|
+
}
|
|
35
61
|
/**
|
|
36
62
|
* The persistent chrome shared by every webmail app (Mail, Calendar, Contacts, Tasks): a left icon rail for
|
|
37
63
|
* switching apps (full page loads — see `MailShell`'s original doc comment on this framework having no
|
|
@@ -39,17 +65,19 @@ const ACTIVE_LABELS = {
|
|
|
39
65
|
* Each app's own shell (e.g. `MailShell`) renders its own contextual sidebar + content as `children`, inside
|
|
40
66
|
* the area to the right of the icon rail and below the header.
|
|
41
67
|
*/
|
|
42
|
-
export default function AppShell({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, children, }) {
|
|
68
|
+
export default function AppShell({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }) {
|
|
43
69
|
const [stoppingImpersonation, setStoppingImpersonation] = useState(false);
|
|
70
|
+
const signingOutRef = useRef(false);
|
|
44
71
|
const { branding, iconSrc } = useBranding();
|
|
45
72
|
useRedirectIfUnauthenticated(userUid, authServerUrl);
|
|
46
73
|
// Mounted here, not scoped to Mail/Settings (the only shells that actually read unlocked keys),
|
|
47
74
|
// specifically so activity in *any* app resets the idle clock - see that hook's own doc comment.
|
|
48
75
|
useIdleKeyTimeout();
|
|
49
|
-
// An administrator on a server that hasn't finished first-run setup is sent to the setup wizard.
|
|
50
|
-
//
|
|
76
|
+
// An administrator on a server that hasn't finished first-run setup is sent to the setup wizard. The status
|
|
77
|
+
// check is admin-only, so it's only made for a trusted caller - everyone else would just get a 403 on every
|
|
78
|
+
// page load. Any failure is ignored.
|
|
51
79
|
useEffect(() => {
|
|
52
|
-
if (!userUid || impersonating) {
|
|
80
|
+
if (!userUid || impersonating || !trusted) {
|
|
53
81
|
return;
|
|
54
82
|
}
|
|
55
83
|
getSetupStatus()
|
|
@@ -59,14 +87,54 @@ export default function AppShell({ active, userUid, authServerUrl, impersonating
|
|
|
59
87
|
}
|
|
60
88
|
})
|
|
61
89
|
.catch(() => undefined);
|
|
62
|
-
}, [userUid, impersonating]);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
90
|
+
}, [userUid, impersonating, trusted]);
|
|
91
|
+
// Another tab signing out (this app's `handleSignOut`, or the admin/escrow consoles' `signOutOfConsole()`)
|
|
92
|
+
// ended this session too - its auth cookie is gone - so this tab destroys its own unlocked keys and every
|
|
93
|
+
// local search index on the device, then leaves as well. The consoles have no local-index client of their
|
|
94
|
+
// own, so this is what actually removes the indexes after a console sign-out (the console also records a
|
|
95
|
+
// pending deletion, retried on the next mail load, in case no mail tab is open). `destroyAllLocalIndexes()`
|
|
96
|
+
// announces the sign-out on this same channel, which this tab then hears itself, so the ref is set first:
|
|
97
|
+
// each tab reacts once, and the tab that started the sign-out ignores its own announcement.
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!userUid || typeof BroadcastChannel === "undefined") {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const channel = new BroadcastChannel(SIGN_OUT_CHANNEL);
|
|
103
|
+
channel.addEventListener("message", (event) => {
|
|
104
|
+
if (event.data?.type !== "sign-out" || signingOutRef.current) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
signingOutRef.current = true;
|
|
108
|
+
// Compose windows must not ask "Leave site?" - that would let this forced navigation be cancelled.
|
|
109
|
+
markSigningOut();
|
|
110
|
+
destroyUnlockedKeys();
|
|
111
|
+
clearPinnedSignerCache();
|
|
112
|
+
// Bounded by its own timeout and never rejects - awaited so navigating doesn't kill the Worker mid-delete.
|
|
113
|
+
void destroyAllLocalIndexes().then(() => {
|
|
114
|
+
window.location.href = authServerUrl ?? "/";
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
return () => channel.close();
|
|
118
|
+
}, [userUid, authServerUrl]);
|
|
119
|
+
async function handleSignOut() {
|
|
120
|
+
signingOutRef.current = true;
|
|
121
|
+
// Before flushing and navigating: compose windows then skip their "Leave site?" prompt, which could
|
|
122
|
+
// otherwise cancel the sign-out's own navigation.
|
|
123
|
+
markSigningOut();
|
|
124
|
+
// Unlocked private keys never outlive an explicit sign-out.
|
|
125
|
+
destroyUnlockedKeys();
|
|
126
|
+
// Trusted signer pins read from contacts don't outlive the session either.
|
|
127
|
+
clearPinnedSignerCache();
|
|
128
|
+
// Open compose windows save edits still waiting on their autosave debounce while the session is still
|
|
129
|
+
// valid - logout invalidates it. Bounded the same way as logout itself, and never rejects.
|
|
130
|
+
await flushComposeDrafts(LOGOUT_TIMEOUT_MS);
|
|
131
|
+
// The Tier 2 local index MUST be destroyed on explicit logout, the same as unlocked keys themselves
|
|
132
|
+
// (spec §11). Destroys every index on this device (not only mailboxes opened this page load) and is
|
|
133
|
+
// awaited before navigating - a navigation tears down the Worker mid-delete otherwise.
|
|
134
|
+
// destroyAllLocalIndexes() is bounded by its own timeout and never rejects, so sign-out can't hang.
|
|
135
|
+
// In parallel, auth-server's logout clears the auth cookie and invalidates the session's refresh
|
|
136
|
+
// token - without it, "Sign Out" would only navigate away from a still-valid session.
|
|
137
|
+
await Promise.all([destroyAllLocalIndexes(), logOutOfAuthServer(authServerUrl)]);
|
|
70
138
|
window.location.href = authServerUrl ?? "/";
|
|
71
139
|
}
|
|
72
140
|
async function handleStopImpersonating() {
|
|
@@ -86,10 +154,13 @@ export default function AppShell({ active, userUid, authServerUrl, impersonating
|
|
|
86
154
|
if (!userUid) {
|
|
87
155
|
return _jsx("div", { className: "min-h-screen" });
|
|
88
156
|
}
|
|
89
|
-
|
|
157
|
+
const apps = appRailItems(pluginNav);
|
|
158
|
+
// The header title: "settings" has no rail item, everything else is labelled by its own rail item.
|
|
159
|
+
const activeLabel = active === "settings" ? "Settings" : apps.find((app) => app.id === active)?.label;
|
|
160
|
+
return (_jsx(UnlockPromptProvider, { children: _jsx(ComposeProvider, { userUid: userUid, trusted: !!trusted && !impersonating, children: _jsxs("div", { className: "min-h-screen flex flex-col bg-surface-alt", children: [_jsx(BrandingHeader, { branding: branding }), impersonating && (_jsxs("div", { className: "h-10 shrink-0 bg-warning text-warning-contrast flex items-center justify-center gap-3 text-sm font-medium px-4", children: [_jsxs("span", { children: ["You are viewing as ", _jsx("strong", { children: userUid }), "."] }), _jsx("button", { type: "button", onClick: handleStopImpersonating, disabled: stoppingImpersonation, className: "underline hover:no-underline disabled:opacity-60", children: stoppingImpersonation ? "Returning to admin…" : "Return to admin" })] })), _jsxs("div", { className: "flex-1 flex min-h-0", children: [_jsxs("nav", { "aria-label": "Apps", className: "hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center py-3 gap-1", children: [_jsx("img", { src: iconSrc, width: "96", height: "96", alt: "", className: "mb-3" }), apps.map(({ id, href, label, icon: Icon }) => (_jsx("a", { href: href, "aria-label": label, "aria-current": id === active ? "page" : undefined, title: label, className: [
|
|
90
161
|
"w-10 h-10 flex items-center justify-center rounded-sm",
|
|
91
162
|
id === active
|
|
92
163
|
? "bg-primary/10 text-primary-dark"
|
|
93
164
|
: "text-text-muted hover:bg-surface-alt hover:text-text",
|
|
94
|
-
].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] }), _jsx(BottomTabBar, { apps:
|
|
165
|
+
].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] }), _jsx(BottomTabBar, { apps: apps, active: active }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [_jsxs("header", { className: "h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6", children: [_jsx("span", { className: "font-display font-bold text-lg uppercase tracking-wide", children: activeLabel }), _jsx(UserMenu, { userUid: userUid, authServerUrl: authServerUrl, onSignOut: handleSignOut, showAdminLink: trusted, showSettingsLink: true })] }), _jsx("div", { className: "flex-1 flex min-h-0 pb-14 md:pb-0", children: children })] })] }), _jsx(BrandingFooter, { branding: branding })] }) }) }));
|
|
95
166
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Branding } from "@rapidmx/react-shared/branding/brandingApi.js";
|
|
3
|
+
/**
|
|
4
|
+
* Sanitizes admin-configured branding HTML before it's rendered. The server sanitizes on save too; this also
|
|
5
|
+
* covers anything stored before that. Returns `""` where there's no DOM to sanitize with (server-side
|
|
6
|
+
* rendering), so unsanitized HTML is never emitted.
|
|
7
|
+
*/
|
|
8
|
+
export declare function sanitizeBrandingHtml(html: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Renders the admin-configured `Branding.headerHtml`/`footerHtml` (see `useBranding()`), sanitized - shared by
|
|
11
|
+
* the webmail and admin chromes. The escrow console deliberately renders no branding HTML at all.
|
|
12
|
+
*/
|
|
13
|
+
export declare function BrandingHeader({ branding }: {
|
|
14
|
+
branding: Branding | null;
|
|
15
|
+
}): React.JSX.Element;
|
|
16
|
+
export declare function BrandingFooter({ branding }: {
|
|
17
|
+
branding: Branding | null;
|
|
18
|
+
}): React.JSX.Element;
|
|
@@ -1,18 +1,53 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import DOMPurify from "dompurify";
|
|
3
|
+
/** Elements that could run script, restyle the whole page, collect input, or embed another page - stripped on
|
|
4
|
+
* top of DOMPurify's defaults (which already drop `<script>`, `on*` handlers, and `javascript:` URLs). */
|
|
5
|
+
const FORBIDDEN_TAGS = [
|
|
6
|
+
"script",
|
|
7
|
+
"style",
|
|
8
|
+
"form",
|
|
9
|
+
"input",
|
|
10
|
+
"button",
|
|
11
|
+
"textarea",
|
|
12
|
+
"select",
|
|
13
|
+
"option",
|
|
14
|
+
"iframe",
|
|
15
|
+
"frame",
|
|
16
|
+
"frameset",
|
|
17
|
+
"object",
|
|
18
|
+
"embed",
|
|
19
|
+
"link",
|
|
20
|
+
"meta",
|
|
21
|
+
"base",
|
|
22
|
+
"svg",
|
|
23
|
+
"math",
|
|
24
|
+
];
|
|
25
|
+
const FORBIDDEN_ATTRS = ["action", "formaction", "srcdoc"];
|
|
2
26
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
27
|
+
* Sanitizes admin-configured branding HTML before it's rendered. The server sanitizes on save too; this also
|
|
28
|
+
* covers anything stored before that. Returns `""` where there's no DOM to sanitize with (server-side
|
|
29
|
+
* rendering), so unsanitized HTML is never emitted.
|
|
6
30
|
*/
|
|
7
|
-
export function
|
|
8
|
-
if (!
|
|
9
|
-
return
|
|
31
|
+
export function sanitizeBrandingHtml(html) {
|
|
32
|
+
if (!DOMPurify.isSupported) {
|
|
33
|
+
return "";
|
|
10
34
|
}
|
|
11
|
-
return
|
|
35
|
+
return DOMPurify.sanitize(html, { FORBID_TAGS: FORBIDDEN_TAGS, FORBID_ATTR: FORBIDDEN_ATTRS });
|
|
12
36
|
}
|
|
13
|
-
|
|
14
|
-
|
|
37
|
+
function BrandingHtml({ html }) {
|
|
38
|
+
const clean = html ? sanitizeBrandingHtml(html) : "";
|
|
39
|
+
if (!clean) {
|
|
15
40
|
return null;
|
|
16
41
|
}
|
|
17
|
-
return _jsx("div", { dangerouslySetInnerHTML: { __html:
|
|
42
|
+
return _jsx("div", { dangerouslySetInnerHTML: { __html: clean } });
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Renders the admin-configured `Branding.headerHtml`/`footerHtml` (see `useBranding()`), sanitized - shared by
|
|
46
|
+
* the webmail and admin chromes. The escrow console deliberately renders no branding HTML at all.
|
|
47
|
+
*/
|
|
48
|
+
export function BrandingHeader({ branding }) {
|
|
49
|
+
return _jsx(BrandingHtml, { html: branding?.headerHtml });
|
|
50
|
+
}
|
|
51
|
+
export function BrandingFooter({ branding }) {
|
|
52
|
+
return _jsx(BrandingHtml, { html: branding?.footerHtml });
|
|
18
53
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { PublicKey } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
|
|
3
|
+
export interface KeyEnrollmentGateProps {
|
|
4
|
+
/** Undefined while the caller's mailbox hasn't resolved yet (or has none) - renders `children`
|
|
5
|
+
* unchanged in that case, so this component can be mounted unconditionally at a stable tree
|
|
6
|
+
* position (see this component's own doc comment for why that matters). */
|
|
7
|
+
mailboxUid?: string;
|
|
8
|
+
mailboxAddress?: string;
|
|
9
|
+
/** The mailbox's currently-published public keys (`Mailbox.keys`) - needed to unlock an
|
|
10
|
+
* already-enrolled vault (see `unlockWithPassword()`'s own signature), not just to provision a new
|
|
11
|
+
* one. Treated as empty when undefined - a mailbox with no public keys published yet has nothing to
|
|
12
|
+
* unlock, so this only affects the already-enrolled path. */
|
|
13
|
+
mailboxKeys?: PublicKey[];
|
|
14
|
+
/**
|
|
15
|
+
* `true` (default) preserves this component's original behavior: a mailbox with an existing vault
|
|
16
|
+
* but no unlocked session blocks `children` behind a full-page unlock form. Set to `false` for a
|
|
17
|
+
* mount point that shouldn't block merely because a mailbox resolved - `MailShell` does this, since
|
|
18
|
+
* unlocking is only actually required to sign/encrypt a compose, read an already-encrypted message,
|
|
19
|
+
* or change encryption settings (see `UnlockPromptProvider.tsx`'s `useUnlockPrompt()`, which those
|
|
20
|
+
* specific call sites use instead). Has no effect on first-time provisioning
|
|
21
|
+
* (`setup_password`/`enrolling`/`show_recovery_codes`), which still always blocks regardless - that
|
|
22
|
+
* only ever happens once per mailbox and is a genuine prerequisite, not the source of "unlock keeps
|
|
23
|
+
* popping up" friction this prop exists to avoid.
|
|
24
|
+
*/
|
|
25
|
+
blocking?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* `true` when the caller may provision this mailbox's first encryption key - i.e. they own it and aren't
|
|
28
|
+
* an administrator impersonating them. `false` (default, so a caller that forgets it never provisions) for
|
|
29
|
+
* a shared/delegated mailbox the caller doesn't own, or an impersonated session: a mailbox with no vault yet then
|
|
30
|
+
* renders `children` instead of the "Protect your mailbox" setup, so a delegate never enrolls keys
|
|
31
|
+
* (and recovery codes) for someone else's mailbox. Unlocking an existing vault is unaffected.
|
|
32
|
+
*/
|
|
33
|
+
canProvision?: boolean;
|
|
34
|
+
children: React.ReactNode;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Gates a mailbox's normal content behind one-time E2E encryption key setup, per
|
|
38
|
+
* `specs/end-to-end_encryption.md`'s "Generate signing and encryption keypairs on the client's device on
|
|
39
|
+
* first sign-in." Rendered by `MailShell` wrapping its own returned content unconditionally, not only
|
|
40
|
+
* once a mailbox is resolved — mounting this component at a *different* tree position depending on
|
|
41
|
+
* `mailboxUid`'s readiness (e.g. only wrapping once ready, rendering bare content beforehand) would make
|
|
42
|
+
* `children` (real `AppShell` chrome) itself remount the moment `mailboxUid` resolves, tearing down any
|
|
43
|
+
* state/effects it had already started. Always wrapping keeps `AppShell` mounted continuously across
|
|
44
|
+
* that transition; this component simply passes `children` through untouched while `mailboxUid` is
|
|
45
|
+
* still unresolved, and only starts its own check once a real `mailboxUid` is supplied.
|
|
46
|
+
*
|
|
47
|
+
* A `getKeyVault()` failure (network error, server not yet wired up to serve this endpoint) also renders
|
|
48
|
+
* `children` unchanged rather than blocking mail entirely - encryption is optional and gradual by design
|
|
49
|
+
* (the spec's own "near zero frictionless experience" goal), not a hard prerequisite for reading mail.
|
|
50
|
+
*/
|
|
51
|
+
export default function KeyEnrollmentGate({ mailboxUid, mailboxAddress, mailboxKeys, blocking, canProvision, children, }: KeyEnrollmentGateProps): React.JSX.Element | null;
|
|
@@ -8,7 +8,9 @@ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
|
8
8
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
9
9
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
10
10
|
import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
|
|
11
|
-
import { enrollKey, getKeyVault } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
|
|
11
|
+
import { enrollKey, getKeyVault, VaultAlreadyInitializedError } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
|
|
12
|
+
import { UnopenableKeysNotice, unlockErrorMessage } from "./UnlockPromptProvider.js";
|
|
13
|
+
import { RecoveryFollowUpModal, UnlockModeToggle, startRecoveryUnlock } from "./RecoveryCodeUnlock.js";
|
|
12
14
|
import { ENCRYPTION_PRIVATE_KEY_AAD_PURPOSE, getUnlockedKeys, unlockWithPassword, } from "@rapidmx/react-shared/crypto/keySession.js";
|
|
13
15
|
import { buildAad, generateMasterKey, sealWithKey } from "@rapidmx/react-shared/crypto/masterKey.js";
|
|
14
16
|
import { buildPasswordWrap, buildRecoveryWraps } from "@rapidmx/react-shared/crypto/masterKeyWraps.js";
|
|
@@ -48,7 +50,7 @@ async function provisionEncryptionKey(mailboxUid, mailboxAddress, password) {
|
|
|
48
50
|
* `children` unchanged rather than blocking mail entirely - encryption is optional and gradual by design
|
|
49
51
|
* (the spec's own "near zero frictionless experience" goal), not a hard prerequisite for reading mail.
|
|
50
52
|
*/
|
|
51
|
-
export default function KeyEnrollmentGate({ mailboxUid, mailboxAddress, mailboxKeys, blocking = true, children, }) {
|
|
53
|
+
export default function KeyEnrollmentGate({ mailboxUid, mailboxAddress, mailboxKeys, blocking = true, canProvision = false, children, }) {
|
|
52
54
|
const [status, setStatus] = useState(mailboxUid ? "checking" : "ready");
|
|
53
55
|
const [password, setPassword] = useState("");
|
|
54
56
|
const [confirmPassword, setConfirmPassword] = useState("");
|
|
@@ -56,6 +58,11 @@ export default function KeyEnrollmentGate({ mailboxUid, mailboxAddress, mailboxK
|
|
|
56
58
|
const [recoveryCodes, setRecoveryCodes] = useState([]);
|
|
57
59
|
const [codesSaved, setCodesSaved] = useState(false);
|
|
58
60
|
const [codesCopied, setCodesCopied] = useState(false);
|
|
61
|
+
const [unopenableKeys, setUnopenableKeys] = useState(null);
|
|
62
|
+
const [unlockMode, setUnlockMode] = useState("password");
|
|
63
|
+
const [recoveryCode, setRecoveryCode] = useState("");
|
|
64
|
+
// Set after a recovery-code unlock: `children` render at once, with the follow-up steps in a dialog on top.
|
|
65
|
+
const [followUp, setFollowUp] = useState(null);
|
|
59
66
|
useEffect(() => {
|
|
60
67
|
if (!mailboxUid) {
|
|
61
68
|
return;
|
|
@@ -71,7 +78,7 @@ export default function KeyEnrollmentGate({ mailboxUid, mailboxAddress, mailboxK
|
|
|
71
78
|
getKeyVault(mailboxUid)
|
|
72
79
|
.then((vault) => {
|
|
73
80
|
if (!cancelled) {
|
|
74
|
-
setStatus(vault.wrappedKeys.length > 0 ? "unlock" : "setup_password");
|
|
81
|
+
setStatus(vault.wrappedKeys.length > 0 ? "unlock" : canProvision ? "setup_password" : "ready");
|
|
75
82
|
}
|
|
76
83
|
})
|
|
77
84
|
.catch(() => {
|
|
@@ -82,7 +89,7 @@ export default function KeyEnrollmentGate({ mailboxUid, mailboxAddress, mailboxK
|
|
|
82
89
|
return () => {
|
|
83
90
|
cancelled = true;
|
|
84
91
|
};
|
|
85
|
-
}, [mailboxUid]);
|
|
92
|
+
}, [mailboxUid, canProvision]);
|
|
86
93
|
async function handleUnlock(e) {
|
|
87
94
|
e.preventDefault();
|
|
88
95
|
setError(null);
|
|
@@ -90,14 +97,21 @@ export default function KeyEnrollmentGate({ mailboxUid, mailboxAddress, mailboxK
|
|
|
90
97
|
try {
|
|
91
98
|
// Only reachable via "unlock", which the effect above only ever sets once mailboxUid was
|
|
92
99
|
// defined.
|
|
93
|
-
await
|
|
100
|
+
const result = await (unlockMode === "recovery"
|
|
101
|
+
? startRecoveryUnlock(mailboxUid, mailboxKeys ?? [], recoveryCode)
|
|
102
|
+
: unlockWithPassword(mailboxUid, mailboxKeys ?? [], password));
|
|
103
|
+
const recovery = unlockMode === "recovery" ? result : null;
|
|
104
|
+
if (result.unopenableKeys.length > 0) {
|
|
105
|
+
setUnopenableKeys(result.unopenableKeys);
|
|
106
|
+
}
|
|
107
|
+
setRecoveryCode("");
|
|
108
|
+
setFollowUp(recovery);
|
|
94
109
|
setStatus("ready");
|
|
95
110
|
}
|
|
96
|
-
catch {
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
setError("Incorrect password.");
|
|
111
|
+
catch (err) {
|
|
112
|
+
// "Incorrect password." (or "That recovery code didn't work.") unless the secret was right but the
|
|
113
|
+
// encryption key won't open - see `unlockErrorMessage()`.
|
|
114
|
+
setError(unlockErrorMessage(err, unlockMode));
|
|
101
115
|
setStatus("unlock");
|
|
102
116
|
}
|
|
103
117
|
}
|
|
@@ -114,13 +128,28 @@ export default function KeyEnrollmentGate({ mailboxUid, mailboxAddress, mailboxK
|
|
|
114
128
|
setError(null);
|
|
115
129
|
setStatus("enrolling");
|
|
116
130
|
try {
|
|
131
|
+
// The vault was only checked on mount - another tab or device may have set encryption up since.
|
|
132
|
+
// Provisioning now would publish a second master key, orphaning whichever setup lost the race, so
|
|
133
|
+
// re-check right before generating anything. restapi's enrollKey() also refuses (409) wraps for a
|
|
134
|
+
// vault that already has them, for a race inside this window.
|
|
117
135
|
// Only reachable via "setup_password", which the effect above only ever sets once mailboxUid
|
|
118
136
|
// was defined (and mailboxAddress necessarily came with it - see KeyEnrollmentGateProps).
|
|
137
|
+
const fresh = await getKeyVault(mailboxUid);
|
|
138
|
+
if (fresh.wrappedKeys.length > 0 || fresh.masterKeyWraps.length > 0) {
|
|
139
|
+
setStatus("already_set_up");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
119
142
|
const { recoveryCodes: codes } = await provisionEncryptionKey(mailboxUid, mailboxAddress, password);
|
|
120
143
|
setRecoveryCodes(codes);
|
|
121
144
|
setStatus("show_recovery_codes");
|
|
122
145
|
}
|
|
123
146
|
catch (err) {
|
|
147
|
+
// Only a 409 that is really "the vault already has wraps" - an unrelated lost optimistic-lock race is shown
|
|
148
|
+
// as an ordinary error.
|
|
149
|
+
if (err instanceof VaultAlreadyInitializedError) {
|
|
150
|
+
setStatus("already_set_up");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
124
153
|
setError(err instanceof ApiRequestError ? err.message : "Could not set up encryption for this mailbox.");
|
|
125
154
|
setStatus("setup_password");
|
|
126
155
|
}
|
|
@@ -141,14 +170,22 @@ export default function KeyEnrollmentGate({ mailboxUid, mailboxAddress, mailboxK
|
|
|
141
170
|
}
|
|
142
171
|
if ((status === "unlock" || status === "unlocking") && blocking) {
|
|
143
172
|
const unlocking = status === "unlocking";
|
|
144
|
-
return (_jsx("div", { className: "min-h-screen flex items-center justify-center p-8 bg-surface-alt", children: _jsxs("div", { className: "w-full max-w-md bg-surface border border-border rounded-md p-8", children: [_jsx("h1", { className: "text-lg font-bold mb-2", children: "Unlock your mailbox" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children:
|
|
173
|
+
return (_jsx("div", { className: "min-h-screen flex items-center justify-center p-8 bg-surface-alt", children: _jsxs("div", { className: "w-full max-w-md bg-surface border border-border rounded-md p-8", children: [_jsx("h1", { className: "text-lg font-bold mb-2", children: "Unlock your mailbox" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: unlockMode === "password"
|
|
174
|
+
? "Enter your encryption password to unlock signing and reading protected mail this session."
|
|
175
|
+
: "Enter one of your recovery codes to unlock signing and reading protected mail this session. Each code works once." }), error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: handleUnlock, children: [unlockMode === "password" ? (_jsx(FormField, { label: "Encryption password", htmlFor: "key-unlock-password", children: _jsx("input", { id: "key-unlock-password", type: "password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: password, onChange: (e) => setPassword(e.target.value), disabled: unlocking, autoComplete: "current-password" }) })) : (_jsx(FormField, { label: "Recovery code", htmlFor: "key-unlock-recovery-code", children: _jsx("input", { id: "key-unlock-recovery-code", type: "text", className: "w-full text-sm font-mono border border-border rounded-sm py-1.5 px-2 bg-surface", value: recoveryCode, onChange: (e) => setRecoveryCode(e.target.value), disabled: unlocking, autoComplete: "off", autoCapitalize: "characters", spellCheck: false }) })), _jsx(Button, { type: "submit", loading: unlocking, disabled: unlocking, children: "Unlock" })] }), _jsx("div", { className: "mt-3", children: _jsx(UnlockModeToggle, { mode: unlockMode, disabled: unlocking, onChange: (next) => {
|
|
176
|
+
setUnlockMode(next);
|
|
177
|
+
setError(null);
|
|
178
|
+
} }) })] }) }));
|
|
145
179
|
}
|
|
146
180
|
if (status === "setup_password" || status === "enrolling") {
|
|
147
181
|
const enrolling = status === "enrolling";
|
|
148
182
|
return (_jsx("div", { className: "min-h-screen flex items-center justify-center p-8 bg-surface-alt", children: _jsxs("div", { className: "w-full max-w-md bg-surface border border-border rounded-md p-8", children: [_jsx("h1", { className: "text-lg font-bold mb-2", children: "Protect your mailbox" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Choose a password to protect your encryption keys. This is separate from your sign-in password and is never sent to the server." }), error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: handleSetPassword, children: [_jsx(FormField, { label: "Encryption password", htmlFor: "key-enrollment-password", children: _jsx("input", { id: "key-enrollment-password", type: "password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: password, onChange: (e) => setPassword(e.target.value), disabled: enrolling, autoComplete: "new-password" }) }), _jsx(FormField, { label: "Confirm password", htmlFor: "key-enrollment-password-confirm", children: _jsx("input", { id: "key-enrollment-password-confirm", type: "password", className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), disabled: enrolling, autoComplete: "new-password" }) }), _jsx(Button, { type: "submit", loading: enrolling, disabled: enrolling, children: "Continue" })] })] }) }));
|
|
149
183
|
}
|
|
184
|
+
if (status === "already_set_up") {
|
|
185
|
+
return (_jsx("div", { className: "min-h-screen flex items-center justify-center p-8 bg-surface-alt", children: _jsxs("div", { className: "w-full max-w-md bg-surface border border-border rounded-md p-8", children: [_jsx("h1", { className: "text-lg font-bold mb-2", children: "Encryption is already set up" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "This mailbox\u2019s encryption was set up in another tab or on another device while this page was open, so nothing was changed here. Reload the page and unlock with the password chosen there." }), _jsx(Button, { type: "button", onClick: () => window.location.reload(), children: "Reload" })] }) }));
|
|
186
|
+
}
|
|
150
187
|
if (status === "show_recovery_codes") {
|
|
151
188
|
return (_jsx("div", { className: "min-h-screen flex items-center justify-center p-8 bg-surface-alt", children: _jsxs("div", { className: "w-full max-w-md bg-surface border border-border rounded-md p-8", children: [_jsx("h1", { className: "text-lg font-bold mb-2", children: "Save your recovery codes" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "If you lose your password, these codes are the only way to recover your encrypted mail. Each code can be used once. Store them somewhere safe \u2014 they will not be shown again." }), _jsx("ul", { className: "grid grid-cols-2 gap-2 mb-3 font-mono text-sm", children: recoveryCodes.map((code) => (_jsx("li", { className: "bg-surface-alt rounded-sm py-1.5 px-2 text-center", children: code }, code))) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto mb-5", onClick: handleCopyCodes, children: codesCopied ? "Copied" : "Copy codes to clipboard" }), _jsxs("label", { className: "flex items-center gap-2 text-sm mb-4", children: [_jsx("input", { type: "checkbox", checked: codesSaved, onChange: (e) => setCodesSaved(e.target.checked) }), "I have saved these recovery codes in a safe place."] }), _jsx(Button, { type: "button", disabled: !codesSaved, onClick: () => setStatus("ready"), children: "Continue" })] }) }));
|
|
152
189
|
}
|
|
153
|
-
return
|
|
190
|
+
return (_jsxs(_Fragment, { children: [children, unopenableKeys && _jsx(UnopenableKeysNotice, { fingerprints: unopenableKeys, onDismiss: () => setUnopenableKeys(null) }), followUp && _jsx(RecoveryFollowUpModal, { followUp: followUp, onDone: () => setFollowUp(null) })] }));
|
|
154
191
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Rendered by each app's shell (Mail/Calendar/Contacts/Tasks) *instead of* the normal `AppShell`
|
|
4
|
+
* chrome (icon rail, header, folder tree, ...) when the caller has no mailbox — a full-screen,
|
|
5
|
+
* 404-style takeover, not a small message nested inside the rest of the client UI, since there's
|
|
6
|
+
* nothing else in the app for a mailbox-less caller to do yet. Attempts self-service
|
|
7
|
+
* auto-provisioning — see `BaseMailboxRoute.autoProvision()`'s own doc comment in `@rapidmx/restapi`
|
|
8
|
+
* for the full contract — which is itself a no-op (a 404) unless an admin has both turned it on
|
|
9
|
+
* (`mail:auto_provision:enabled`) and configured at least one domain (`mail:domains`). Safe to render
|
|
10
|
+
* unconditionally in that place: any failure (disabled, no registered username, the identity service
|
|
11
|
+
* unreachable) just falls back to the same plain "no mailbox" message this replaces.
|
|
12
|
+
*/
|
|
13
|
+
export default function MailboxProvisioning(): React.JSX.Element;
|
|
@@ -24,7 +24,10 @@ export default function MailboxProvisioning() {
|
|
|
24
24
|
const [options, setOptions] = useState([]);
|
|
25
25
|
const [selected, setSelected] = useState("");
|
|
26
26
|
const [error, setError] = useState(null);
|
|
27
|
+
// Bumped by "Retry" to re-run the check below.
|
|
28
|
+
const [attempt, setAttempt] = useState(0);
|
|
27
29
|
useEffect(() => {
|
|
30
|
+
setStatus("checking");
|
|
28
31
|
autoProvisionMailbox()
|
|
29
32
|
.then((result) => {
|
|
30
33
|
if (result.status === "needs_selection") {
|
|
@@ -42,8 +45,10 @@ export default function MailboxProvisioning() {
|
|
|
42
45
|
window.location.reload();
|
|
43
46
|
}
|
|
44
47
|
})
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
// A 503 means the server couldn't read its own provisioning policy right now - not that there's no
|
|
49
|
+
// mailbox to be had - so it gets a retry instead of the permanent "ask an administrator" message.
|
|
50
|
+
.catch((err) => setStatus(err instanceof ApiRequestError && err.status === 503 ? "retryable" : "unavailable"));
|
|
51
|
+
}, [attempt]);
|
|
47
52
|
async function handleConfirm() {
|
|
48
53
|
// `selected` only ever takes a value from `options` itself (the initial default, or the
|
|
49
54
|
// <select>'s own onChange, which can only report one of its own rendered <option> values), so
|
|
@@ -68,6 +73,9 @@ export default function MailboxProvisioning() {
|
|
|
68
73
|
const creating = status === "creating";
|
|
69
74
|
content = (_jsxs("div", { className: "w-full flex flex-col gap-3", children: [_jsx("p", { className: "text-sm text-text-muted text-center", children: "Choose your mailbox address to get started:" }), error && _jsx(Alert, { children: error }), _jsx("select", { "aria-label": "Mailbox address", className: "text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: selected, onChange: (e) => setSelected(e.target.value), disabled: creating, children: options.map((option) => (_jsx("option", { value: option.primarySmtpAddress, children: option.primarySmtpAddress }, option.primarySmtpAddress))) }), _jsx(Button, { type: "button", onClick: handleConfirm, loading: creating, disabled: creating, className: "!w-auto self-center", children: "Continue" })] }));
|
|
70
75
|
}
|
|
76
|
+
else if (status === "retryable") {
|
|
77
|
+
content = (_jsxs(_Fragment, { children: [_jsx("h1", { className: "text-lg font-bold uppercase tracking-wide", children: "Couldn\u2019t check right now" }), _jsx("p", { className: "text-sm text-text-muted", children: "We couldn\u2019t check whether a mailbox can be set up for you. Please try again." }), _jsx(Button, { type: "button", onClick: () => setAttempt((n) => n + 1), className: "!w-auto self-center", children: "Retry" })] }));
|
|
78
|
+
}
|
|
71
79
|
else {
|
|
72
80
|
content = (_jsxs(_Fragment, { children: [_jsx("h1", { className: "text-lg font-bold uppercase tracking-wide", children: "No mailbox available" }), _jsx("p", { className: "text-sm text-text-muted", children: "Ask an administrator to create one for you." })] }));
|
|
73
81
|
}
|