@rapidmx/web-client 0.4.0 → 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.
- 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 +3 -1
- 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 +1 -1
- 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/escrow/layout/EscrowShell.tsx +148 -134
- package/apps/shared/components/layout/AppShell.tsx +87 -15
- 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 +12 -2
- 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/mail/listAllPages.ts +39 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +64 -55
- package/apps/shared/search/localIndexBuilder.ts +481 -179
- package/apps/shared/search/localIndexRpcClient.ts +228 -28
- 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/booking-types/[uid].tsx +306 -300
- 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/audit-log/index.js +38 -8
- package/dist/apps/admin/data-requests/index.js +52 -72
- package/dist/apps/admin/distribution-lists/[uid].js +1 -1
- package/dist/apps/admin/escrow-scopes/[uid].js +91 -12
- package/dist/apps/admin/escrow-scopes/new/index.js +8 -3
- package/dist/apps/admin/index.js +4 -1
- package/dist/apps/admin/ingest-queue/index.js +24 -6
- package/dist/apps/admin/mailboxes/[uid].js +20 -4
- package/dist/apps/admin/quarantine/index.js +20 -11
- package/dist/apps/admin/transport-rules/[uid].js +18 -2
- package/dist/apps/admin/transport-rules/_transportRuleConfig.js +19 -0
- package/dist/apps/admin/transport-rules/index.js +22 -6
- package/dist/apps/admin/transport-rules/new/index.js +18 -2
- package/dist/apps/escrow/_layout.js +3 -3
- package/dist/apps/escrow/audit-log/index.js +26 -1
- package/dist/apps/escrow/matters/[uid].js +77 -42
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +11 -2
- package/dist/apps/shared/components/admin/layout/AdminShell.js +3 -1
- package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +72 -0
- package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +22 -4
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +15 -4
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +13 -7
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +7 -2
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +6 -0
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +34 -12
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +390 -78
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +40 -11
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +29 -13
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +87 -21
- package/dist/apps/shared/components/admin/signOut.js +75 -0
- package/dist/apps/shared/components/admin/usePagedList.js +99 -0
- package/dist/apps/shared/components/calendar/EventModal.js +142 -21
- package/dist/apps/shared/components/calendar/MonthView.js +12 -5
- package/dist/apps/shared/components/calendar/RecurrenceEditor.js +40 -5
- package/dist/apps/shared/components/calendar/SplitDayView.js +10 -4
- package/dist/apps/shared/components/calendar/TimeGridView.js +23 -7
- package/dist/apps/shared/components/calendar/allDay.js +111 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +43 -5
- package/dist/apps/shared/components/contacts/ContactForm.js +32 -10
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +2 -2
- package/dist/apps/shared/components/contacts/KeyChangeReview.js +63 -0
- package/dist/apps/shared/components/contacts/contactKeys.js +64 -0
- package/dist/apps/shared/components/escrow/layout/EscrowShell.js +24 -5
- package/dist/apps/shared/components/layout/AppShell.js +83 -14
- package/dist/apps/shared/components/layout/BrandingChrome.js +45 -10
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +49 -12
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +10 -2
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +174 -0
- package/dist/apps/shared/components/layout/UnlockPromptProvider.js +73 -14
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +50 -12
- package/dist/apps/shared/components/mail/MessageDetailPane.js +441 -36
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +9 -6
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +699 -128
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +50 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +12 -3
- package/dist/apps/shared/components/mail/pinnedSigners.js +98 -0
- package/dist/apps/shared/components/mail/verificationSeals.js +112 -0
- package/dist/apps/shared/components/mail/writableMailboxes.js +87 -0
- package/dist/apps/shared/components/rules/RuleBuilder.js +34 -6
- package/dist/apps/shared/mail/listAllPages.js +25 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.js +58 -54
- package/dist/apps/shared/search/localIndexBuilder.js +292 -37
- package/dist/apps/shared/search/localIndexRpcClient.js +188 -25
- package/dist/apps/shared/search/localIndexSchema.js +12 -6
- package/dist/apps/shared/search/localIndexStorage.js +86 -0
- package/dist/apps/shared/search/localIndexVFS.js +68 -40
- package/dist/apps/shared/search/localIndexWorker.js +519 -173
- package/dist/apps/www/calendar/index.js +28 -10
- package/dist/apps/www/contacts/[uid].js +13 -2
- package/dist/apps/www/contacts/index.js +80 -10
- package/dist/apps/www/index.js +339 -108
- package/dist/apps/www/messages/[uid].js +18 -6
- package/dist/apps/www/settings/auto-reply/index.js +9 -4
- package/dist/apps/www/settings/booking-types/[uid].js +17 -12
- package/dist/apps/www/settings/encryption/index.js +514 -97
- package/dist/apps/www/settings/filters/[uid].js +22 -7
- package/dist/apps/www/settings/filters/new/index.js +8 -2
- package/dist/apps/www/settings/focused-inbox/index.js +3 -1
- package/dist/apps/www/settings/privacy/index.js +57 -22
- package/dist/apps/www/settings/read-receipts/index.js +10 -3
- package/dist/apps/www/settings/sharing/index.js +7 -11
- package/dist/apps/www/tasks/index.js +50 -10
- package/package.json +2 -2
|
@@ -1,98 +1,174 @@
|
|
|
1
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
-
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
-
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
import React, { FormEvent, useState } from "react";
|
|
6
|
-
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
7
|
-
import { MIN_AUDIT_LOG_RETENTION_DAYS, RetentionPolicy, updateRetentionPolicy } from "@rapidmx/react-shared/admin/retentionPolicyApi.js";
|
|
8
|
-
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
9
|
-
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
</
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { FormEvent, useEffect, useState } from "react";
|
|
6
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
7
|
+
import { MIN_AUDIT_LOG_RETENTION_DAYS, RetentionPolicy, RetentionPolicyUpdate, updateRetentionPolicy } from "@rapidmx/react-shared/admin/retentionPolicyApi.js";
|
|
8
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
9
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
10
|
+
import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
11
|
+
|
|
12
|
+
const INPUT_CLASS =
|
|
13
|
+
"w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
14
|
+
|
|
15
|
+
/** Whether saving `next` days newly starts purging (nothing was configured) or purges sooner than `saved` days. */
|
|
16
|
+
function shortensRetention(next: number | null | undefined, saved: number | null | undefined): boolean {
|
|
17
|
+
return next != null && (saved == null || next < saved);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The retention policy editor, shared by the Retention Policy page and the setup wizard. */
|
|
21
|
+
export default function RetentionPolicyForm({
|
|
22
|
+
policy,
|
|
23
|
+
onChange,
|
|
24
|
+
onDirtyChange,
|
|
25
|
+
}: {
|
|
26
|
+
policy: RetentionPolicy;
|
|
27
|
+
onChange: (p: RetentionPolicy) => void;
|
|
28
|
+
/** Told whether the form has edits that haven't been saved. */
|
|
29
|
+
onDirtyChange?: (dirty: boolean) => void;
|
|
30
|
+
}) {
|
|
31
|
+
const [messageRetentionDays, setMessageRetentionDays] = useState(policy.messageRetentionDays?.toString() ?? "");
|
|
32
|
+
const [auditLogRetentionDays, setAuditLogRetentionDays] = useState(policy.auditLogRetentionDays?.toString() ?? "");
|
|
33
|
+
const [error, setError] = useState<string | null>(null);
|
|
34
|
+
const [saving, setSaving] = useState(false);
|
|
35
|
+
const [saved, setSaved] = useState(false);
|
|
36
|
+
|
|
37
|
+
// Edits are unsaved until they match what was loaded or last saved.
|
|
38
|
+
const snapshot = JSON.stringify([messageRetentionDays, auditLogRetentionDays]);
|
|
39
|
+
const [savedSnapshot, setSavedSnapshot] = useState(snapshot);
|
|
40
|
+
const dirty: boolean = snapshot !== savedSnapshot;
|
|
41
|
+
useEffect(() => onDirtyChange?.(dirty), [dirty]);
|
|
42
|
+
|
|
43
|
+
// The retention currently in effect, and a pending save that would newly start or shorten either period - which
|
|
44
|
+
// permanently deletes existing mail or audit-log entries on the next purge run, so it's confirmed first.
|
|
45
|
+
const [savedMessageDays, setSavedMessageDays] = useState(policy.messageRetentionDays);
|
|
46
|
+
const [savedAuditDays, setSavedAuditDays] = useState(policy.auditLogRetentionDays);
|
|
47
|
+
const [pendingPatch, setPendingPatch] = useState<RetentionPolicyUpdate | null>(null);
|
|
48
|
+
|
|
49
|
+
function handleSubmit(e: FormEvent) {
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
// A blank field is sent as `null`, which clears a configured age back to no automatic purge.
|
|
52
|
+
const patch: RetentionPolicyUpdate = {
|
|
53
|
+
messageRetentionDays: messageRetentionDays.trim() === "" ? null : Number(messageRetentionDays),
|
|
54
|
+
auditLogRetentionDays: auditLogRetentionDays.trim() === "" ? null : Number(auditLogRetentionDays),
|
|
55
|
+
};
|
|
56
|
+
if (
|
|
57
|
+
shortensRetention(patch.messageRetentionDays, savedMessageDays) ||
|
|
58
|
+
shortensRetention(patch.auditLogRetentionDays, savedAuditDays)
|
|
59
|
+
) {
|
|
60
|
+
setPendingPatch(patch);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
void save(patch);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function save(patch: RetentionPolicyUpdate) {
|
|
67
|
+
setPendingPatch(null);
|
|
68
|
+
setError(null);
|
|
69
|
+
setSaved(false);
|
|
70
|
+
setSaving(true);
|
|
71
|
+
try {
|
|
72
|
+
const updated = await updateRetentionPolicy(patch);
|
|
73
|
+
setSavedMessageDays(updated.messageRetentionDays);
|
|
74
|
+
setSavedAuditDays(updated.auditLogRetentionDays);
|
|
75
|
+
onChange(updated);
|
|
76
|
+
setSavedSnapshot(snapshot);
|
|
77
|
+
setSaved(true);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not save the retention policy.");
|
|
80
|
+
} finally {
|
|
81
|
+
setSaving(false);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div className="max-w-xl">
|
|
87
|
+
<h1 className="text-xl font-bold uppercase tracking-wide mb-1">Retention Policy</h1>
|
|
88
|
+
<p className="text-sm text-text-muted mb-5">
|
|
89
|
+
Automatically and permanently deletes mail/audit-log entries older than the configured age.
|
|
90
|
+
Leave a field blank for no automatic purge. This does not apply to a mailbox that is a
|
|
91
|
+
custodian on an active legal hold — a held message is skipped and retried on a later run once
|
|
92
|
+
the hold is lifted.
|
|
93
|
+
</p>
|
|
94
|
+
|
|
95
|
+
{error && <Alert>{error}</Alert>}
|
|
96
|
+
{saved && !error && <div className="mb-4 text-sm text-success font-medium">Saved.</div>}
|
|
97
|
+
|
|
98
|
+
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
|
99
|
+
<label className="flex flex-col gap-1.5 text-sm">
|
|
100
|
+
<span className="font-semibold">Message retention (days)</span>
|
|
101
|
+
<input
|
|
102
|
+
aria-label="Message retention (days)"
|
|
103
|
+
type="number"
|
|
104
|
+
min={1}
|
|
105
|
+
className={INPUT_CLASS}
|
|
106
|
+
value={messageRetentionDays}
|
|
107
|
+
onChange={(e) => setMessageRetentionDays(e.target.value)}
|
|
108
|
+
placeholder="No automatic purge"
|
|
109
|
+
/>
|
|
110
|
+
<span className="text-xs text-text-muted">
|
|
111
|
+
Applies to any message, in any folder, regardless of age.
|
|
112
|
+
</span>
|
|
113
|
+
</label>
|
|
114
|
+
<label className="flex flex-col gap-1.5 text-sm">
|
|
115
|
+
<span className="font-semibold">Audit log retention (days)</span>
|
|
116
|
+
<input
|
|
117
|
+
aria-label="Audit log retention (days)"
|
|
118
|
+
type="number"
|
|
119
|
+
min={MIN_AUDIT_LOG_RETENTION_DAYS}
|
|
120
|
+
className={INPUT_CLASS}
|
|
121
|
+
value={auditLogRetentionDays}
|
|
122
|
+
onChange={(e) => setAuditLogRetentionDays(e.target.value)}
|
|
123
|
+
placeholder="Keep forever"
|
|
124
|
+
/>
|
|
125
|
+
<span className="text-xs text-text-muted">
|
|
126
|
+
Cannot be set below {MIN_AUDIT_LOG_RETENTION_DAYS} days. Never applies to the
|
|
127
|
+
tamper-evident escrow audit chain.
|
|
128
|
+
</span>
|
|
129
|
+
</label>
|
|
130
|
+
<div>
|
|
131
|
+
<Button type="submit" loading={saving} disabled={saving} className="!w-auto">
|
|
132
|
+
Save
|
|
133
|
+
</Button>
|
|
134
|
+
</div>
|
|
135
|
+
</form>
|
|
136
|
+
|
|
137
|
+
<Modal open={pendingPatch !== null} onClose={() => setPendingPatch(null)} title="Delete older data?">
|
|
138
|
+
{shortensRetention(pendingPatch?.messageRetentionDays, savedMessageDays) && (
|
|
139
|
+
<p className="text-sm mb-3">
|
|
140
|
+
{savedMessageDays == null
|
|
141
|
+
? "Mail isn't deleted automatically today."
|
|
142
|
+
: `Mail is currently kept for ${savedMessageDays} days.`}{" "}
|
|
143
|
+
With this change, every message older than <strong>{pendingPatch?.messageRetentionDays} days</strong>,
|
|
144
|
+
in every mailbox and folder, is permanently deleted on the next purge run - and so on from then on.
|
|
145
|
+
Mailboxes on an active legal hold are skipped.
|
|
146
|
+
</p>
|
|
147
|
+
)}
|
|
148
|
+
{shortensRetention(pendingPatch?.auditLogRetentionDays, savedAuditDays) && (
|
|
149
|
+
<p className="text-sm mb-3">
|
|
150
|
+
{savedAuditDays == null
|
|
151
|
+
? "Audit-log entries are kept forever today."
|
|
152
|
+
: `Audit-log entries are currently kept for ${savedAuditDays} days.`}{" "}
|
|
153
|
+
With this change, every audit-log entry older than{" "}
|
|
154
|
+
<strong>{pendingPatch?.auditLogRetentionDays} days</strong> is permanently deleted on the next purge
|
|
155
|
+
run. The escrow audit chain is never purged.
|
|
156
|
+
</p>
|
|
157
|
+
)}
|
|
158
|
+
<p className="text-sm font-semibold text-danger mb-4">Deleted data can’t be recovered.</p>
|
|
159
|
+
<div className="flex gap-3 justify-end">
|
|
160
|
+
<Button type="button" variant="secondary" className="!w-auto" onClick={() => setPendingPatch(null)}>
|
|
161
|
+
Cancel
|
|
162
|
+
</Button>
|
|
163
|
+
<Button
|
|
164
|
+
type="button"
|
|
165
|
+
className="!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger"
|
|
166
|
+
onClick={() => void save(pendingPatch!)}
|
|
167
|
+
>
|
|
168
|
+
Save and delete older data
|
|
169
|
+
</Button>
|
|
170
|
+
</div>
|
|
171
|
+
</Modal>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
@@ -13,6 +13,7 @@ import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
|
|
|
13
13
|
import EscrowScopeKeyAndHoldersFields, {
|
|
14
14
|
emptyEscrowScopeKeyAndHoldersValue,
|
|
15
15
|
EscrowScopeKeyAndHoldersValue,
|
|
16
|
+
selfAsHolderError,
|
|
16
17
|
} from "../escrowScopes/EscrowScopeKeyAndHoldersFields.js";
|
|
17
18
|
|
|
18
19
|
const INPUT_CLASS =
|
|
@@ -20,6 +21,9 @@ const INPUT_CLASS =
|
|
|
20
21
|
|
|
21
22
|
type Mode = "none" | "generate" | "existing";
|
|
22
23
|
|
|
24
|
+
/** How long a download's object URL is kept, since some browsers are still reading it after the click returns. */
|
|
25
|
+
export const DOWNLOAD_URL_LIFETIME_MS = 60_000;
|
|
26
|
+
|
|
23
27
|
/** Offers `text` to the browser as a file download. */
|
|
24
28
|
export function downloadTextFile(filename: string, text: string, type: string = "application/x-pem-file"): void {
|
|
25
29
|
const url = URL.createObjectURL(new Blob([text], { type }));
|
|
@@ -29,7 +33,7 @@ export function downloadTextFile(filename: string, text: string, type: string =
|
|
|
29
33
|
document.body.appendChild(link);
|
|
30
34
|
link.click();
|
|
31
35
|
link.remove();
|
|
32
|
-
URL.revokeObjectURL(url);
|
|
36
|
+
setTimeout(() => URL.revokeObjectURL(url), DOWNLOAD_URL_LIFETIME_MS);
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
function fileSafe(name: string): string {
|
|
@@ -42,12 +46,13 @@ function fileSafe(name: string): string {
|
|
|
42
46
|
* generate the key pair here (downloading the private key, which never leaves the browser), paste in an existing
|
|
43
47
|
* certificate, or go without escrow.
|
|
44
48
|
*/
|
|
45
|
-
export default function EscrowSetupStep() {
|
|
49
|
+
export default function EscrowSetupStep({ adminUid }: { adminUid?: string }) {
|
|
46
50
|
const [scopes, setScopes] = useState<EscrowScope[] | null>(null);
|
|
47
51
|
const [mode, setMode] = useState<Mode>("none");
|
|
48
52
|
const [name, setName] = useState("Escrow");
|
|
49
53
|
const [validYears, setValidYears] = useState(5);
|
|
50
54
|
const [generated, setGenerated] = useState<GeneratedEscrowKeys | null>(null);
|
|
55
|
+
const [downloadedPrivateKey, setDownloadedPrivateKey] = useState(false);
|
|
51
56
|
const [savedPrivateKey, setSavedPrivateKey] = useState(false);
|
|
52
57
|
const [keyAndHolders, setKeyAndHolders] = useState<EscrowScopeKeyAndHoldersValue>(emptyEscrowScopeKeyAndHoldersValue());
|
|
53
58
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -63,6 +68,7 @@ export default function EscrowSetupStep() {
|
|
|
63
68
|
setMode(next);
|
|
64
69
|
setError(null);
|
|
65
70
|
setGenerated(null);
|
|
71
|
+
setDownloadedPrivateKey(false);
|
|
66
72
|
setSavedPrivateKey(false);
|
|
67
73
|
setKeyAndHolders(emptyEscrowScopeKeyAndHoldersValue());
|
|
68
74
|
}
|
|
@@ -99,7 +105,17 @@ export default function EscrowSetupStep() {
|
|
|
99
105
|
setError("A name is required.");
|
|
100
106
|
return;
|
|
101
107
|
}
|
|
102
|
-
|
|
108
|
+
// Generated keys are used exactly as generated; only a pasted-in certificate's fields come from the form.
|
|
109
|
+
const publicKey = generated
|
|
110
|
+
? generated.publicKey
|
|
111
|
+
: {
|
|
112
|
+
publicKey: keyAndHolders.publicKey.trim(),
|
|
113
|
+
type: keyAndHolders.keyType.trim(),
|
|
114
|
+
fingerprint: keyAndHolders.fingerprint.trim(),
|
|
115
|
+
notBefore: new Date(keyAndHolders.notBefore).getTime(),
|
|
116
|
+
notAfter: new Date(keyAndHolders.notAfter).getTime(),
|
|
117
|
+
};
|
|
118
|
+
if (!publicKey.publicKey || !publicKey.type || !publicKey.fingerprint) {
|
|
103
119
|
setError("The public key, its type, and its fingerprint are all required.");
|
|
104
120
|
return;
|
|
105
121
|
}
|
|
@@ -111,17 +127,16 @@ export default function EscrowSetupStep() {
|
|
|
111
127
|
setError("Required holders must be between 1 and the number of holders.");
|
|
112
128
|
return;
|
|
113
129
|
}
|
|
130
|
+
const selfError = selfAsHolderError(keyAndHolders.holderUserUids, adminUid);
|
|
131
|
+
if (selfError) {
|
|
132
|
+
setError(selfError);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
114
135
|
setBusy(true);
|
|
115
136
|
try {
|
|
116
137
|
const created = await createEscrowScope({
|
|
117
138
|
name: name.trim(),
|
|
118
|
-
publicKey
|
|
119
|
-
publicKey: keyAndHolders.publicKey.trim(),
|
|
120
|
-
type: keyAndHolders.keyType.trim(),
|
|
121
|
-
fingerprint: keyAndHolders.fingerprint.trim(),
|
|
122
|
-
notBefore: new Date(keyAndHolders.notBefore).getTime(),
|
|
123
|
-
notAfter: new Date(keyAndHolders.notAfter).getTime(),
|
|
124
|
-
},
|
|
139
|
+
publicKey,
|
|
125
140
|
holderUserUids: keyAndHolders.holderUserUids,
|
|
126
141
|
requiredHolders: keyAndHolders.requiredHolders,
|
|
127
142
|
notifySubjectOnAccess: keyAndHolders.notifySubjectOnAccess,
|
|
@@ -138,7 +153,12 @@ export default function EscrowSetupStep() {
|
|
|
138
153
|
|
|
139
154
|
const scopeForm = (
|
|
140
155
|
<form onSubmit={create} className="flex flex-col gap-5">
|
|
141
|
-
<EscrowScopeKeyAndHoldersFields
|
|
156
|
+
<EscrowScopeKeyAndHoldersFields
|
|
157
|
+
value={keyAndHolders}
|
|
158
|
+
onChange={setKeyAndHolders}
|
|
159
|
+
disabled={mode === "generate" && busy}
|
|
160
|
+
keyReadOnly={mode === "generate"}
|
|
161
|
+
/>
|
|
142
162
|
<div>
|
|
143
163
|
<Button type="submit" loading={busy} disabled={busy} className="!w-auto">
|
|
144
164
|
Create escrow scope
|
|
@@ -221,11 +241,19 @@ export default function EscrowSetupStep() {
|
|
|
221
241
|
Download the private key now and give it to your escrow holders. It isn’t stored
|
|
222
242
|
anywhere else - if it’s lost, no escrowed mail can ever be recovered.
|
|
223
243
|
</Alert>
|
|
244
|
+
<Alert>
|
|
245
|
+
You’re downloading this private key, so you must not also be one of its holders:
|
|
246
|
+
pick holders other than yourself, hand the key over, and delete your copy once they
|
|
247
|
+
have it. Otherwise one person could both configure escrow and recover mail with it.
|
|
248
|
+
</Alert>
|
|
224
249
|
<div className="flex flex-wrap gap-2">
|
|
225
250
|
<Button
|
|
226
251
|
type="button"
|
|
227
252
|
className="!w-auto"
|
|
228
|
-
onClick={() =>
|
|
253
|
+
onClick={() => {
|
|
254
|
+
downloadTextFile(`${fileSafe(name)}-private-key.pem`, generated.privateKeyPem);
|
|
255
|
+
setDownloadedPrivateKey(true);
|
|
256
|
+
}}
|
|
229
257
|
>
|
|
230
258
|
Download private key
|
|
231
259
|
</Button>
|
|
@@ -239,9 +267,17 @@ export default function EscrowSetupStep() {
|
|
|
239
267
|
</Button>
|
|
240
268
|
</div>
|
|
241
269
|
<label className="flex items-center gap-2">
|
|
242
|
-
<input
|
|
270
|
+
<input
|
|
271
|
+
type="checkbox"
|
|
272
|
+
checked={savedPrivateKey}
|
|
273
|
+
disabled={!downloadedPrivateKey}
|
|
274
|
+
onChange={(e) => setSavedPrivateKey(e.target.checked)}
|
|
275
|
+
/>
|
|
243
276
|
I’ve saved the private key somewhere safe
|
|
244
277
|
</label>
|
|
278
|
+
{!downloadedPrivateKey && (
|
|
279
|
+
<p className="text-xs text-text-muted">Download the private key before continuing.</p>
|
|
280
|
+
)}
|
|
245
281
|
</div>
|
|
246
282
|
)}
|
|
247
283
|
</div>
|