@rapidmx/web-client 0.8.0 → 0.10.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 +192 -88
- package/apps/shared/auth/accountUrl.ts +9 -0
- package/apps/shared/auth/adminAccess.ts +99 -0
- package/apps/shared/components/admin/elevation.ts +61 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +73 -7
- package/apps/shared/components/admin/settings/BrandingForm.tsx +2 -1
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +87 -24
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +8 -2
- package/apps/shared/components/contacts/ContactsToolbar.tsx +125 -115
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +15 -3
- package/apps/shared/components/layout/AppShell.tsx +411 -288
- package/apps/shared/components/layout/BrandingChrome.tsx +3 -2
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -389
- package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -126
- package/apps/shared/components/layout/UserMenu.tsx +303 -150
- package/apps/shared/components/mail/ConversationList.tsx +269 -253
- package/apps/shared/components/mail/ConversationThreadPane.tsx +475 -434
- package/apps/shared/components/mail/LazyReadingPane.tsx +115 -0
- package/apps/shared/components/mail/MailAddress.tsx +88 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +235 -222
- package/apps/shared/components/mail/MessageDetailPane.tsx +1458 -1383
- package/apps/shared/components/mail/NewMailToasts.tsx +141 -0
- package/apps/shared/components/mail/compose/ComposeContext.tsx +279 -160
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -418
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +1842 -1700
- package/apps/shared/components/mail/compose/ComposeWindowPlaceholder.tsx +111 -0
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -118
- package/apps/shared/components/mail/compose/SendFailureAlert.tsx +48 -0
- package/apps/shared/components/mail/compose/composePerf.ts +46 -0
- package/apps/shared/components/mail/compose/quotedBody.ts +161 -100
- package/apps/shared/components/mail/layout/MailShell.tsx +476 -445
- package/apps/shared/components/mail/unreadStyle.tsx +75 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +252 -240
- package/apps/shared/components/tasks/layout/TasksShell.tsx +15 -3
- package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -0
- package/apps/shared/keyboard/ShortcutProvider.tsx +62 -0
- package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -0
- package/apps/shared/keyboard/dispatch.ts +124 -0
- package/apps/shared/keyboard/format.ts +89 -0
- package/apps/shared/keyboard/keymap.ts +114 -0
- package/apps/shared/keyboard/match.ts +44 -0
- package/apps/shared/keyboard/parse.ts +136 -0
- package/apps/shared/keyboard/platform.ts +34 -0
- package/apps/shared/keyboard/registry.ts +65 -0
- package/apps/shared/keyboard/targets.ts +79 -0
- package/apps/shared/keyboard/useShortcut.ts +50 -0
- package/apps/shared/keyboard/useShortcutProps.ts +17 -0
- package/apps/shared/mail/folderCounts.ts +302 -0
- package/apps/shared/mail/listSnapshots.ts +87 -0
- package/apps/shared/mail/mergeFirstPage.ts +47 -0
- package/apps/shared/mail/messageReadState.ts +85 -0
- package/apps/shared/mail/newMailNotifications.ts +183 -0
- package/apps/shared/mail/useMailConnection.ts +150 -0
- package/apps/shared/mail/useMailLiveUpdates.ts +232 -0
- package/apps/shared/mail/useMarkMessageRead.ts +47 -0
- package/apps/shared/mail/useNewMailNotifications.ts +163 -0
- package/apps/shared/mail/useUnreadTitle.ts +42 -0
- package/apps/shared/navigation/AppRouter.tsx +300 -0
- package/apps/shared/navigation/appHrefs.ts +23 -0
- package/apps/shared/navigation/frameContext.tsx +35 -0
- package/apps/shared/navigation/idle.ts +45 -0
- package/apps/shared/navigation/routerContext.tsx +83 -0
- package/apps/shared/navigation/routes.ts +72 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +114 -98
- package/apps/shared/styles/app.css +28 -10
- package/apps/www/_routedPage.tsx +24 -0
- package/apps/www/_routes.ts +35 -0
- package/apps/www/calendar/index.tsx +55 -19
- package/apps/www/contacts/[uid].tsx +112 -107
- package/apps/www/contacts/index.tsx +584 -567
- package/apps/www/index.tsx +2280 -1854
- package/apps/www/messages/[uid].tsx +106 -101
- package/apps/www/settings/auto-reply/index.tsx +4 -1
- package/apps/www/settings/encryption/index.tsx +1252 -1249
- package/apps/www/settings/filters/[uid].tsx +4 -1
- package/apps/www/settings/filters/index.tsx +102 -99
- package/apps/www/settings/filters/new/index.tsx +138 -133
- package/apps/www/settings/labels/index.tsx +204 -201
- package/apps/www/settings/privacy/index.tsx +4 -1
- package/apps/www/settings/read-receipts/index.tsx +4 -1
- package/apps/www/settings/sharing/index.tsx +277 -274
- package/apps/www/settings/signatures/[uid].tsx +170 -167
- package/apps/www/settings/signatures/index.tsx +88 -85
- package/apps/www/settings/signatures/new/index.tsx +134 -129
- package/apps/www/tasks/index.tsx +18 -2
- package/dist/apps/shared/auth/accountUrl.d.ts +2 -0
- package/dist/apps/shared/auth/accountUrl.js +8 -0
- package/dist/apps/shared/auth/adminAccess.d.ts +30 -0
- package/dist/apps/shared/auth/adminAccess.js +89 -0
- package/dist/apps/shared/components/admin/elevation.d.ts +24 -0
- package/dist/apps/shared/components/admin/elevation.js +56 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +12 -4
- package/dist/apps/shared/components/admin/layout/AdminShell.js +49 -6
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +1 -1
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +31 -13
- package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +1 -1
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +8 -4
- package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
- package/dist/apps/shared/components/contacts/ContactsToolbar.js +7 -4
- package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +1 -1
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +15 -5
- package/dist/apps/shared/components/layout/AppShell.d.ts +29 -4
- package/dist/apps/shared/components/layout/AppShell.js +73 -14
- package/dist/apps/shared/components/layout/BrandingChrome.d.ts +3 -2
- package/dist/apps/shared/components/layout/BrandingChrome.js +3 -2
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +10 -9
- package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +5 -2
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +45 -8
- package/dist/apps/shared/components/layout/UserMenu.d.ts +28 -9
- package/dist/apps/shared/components/layout/UserMenu.js +87 -14
- package/dist/apps/shared/components/mail/ConversationList.js +9 -13
- package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +4 -1
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +53 -28
- package/dist/apps/shared/components/mail/LazyReadingPane.d.ts +9 -0
- package/dist/apps/shared/components/mail/LazyReadingPane.js +82 -0
- package/dist/apps/shared/components/mail/MailAddress.d.ts +27 -0
- package/dist/apps/shared/components/mail/MailAddress.js +39 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +4 -1
- package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -3
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +7 -1
- package/dist/apps/shared/components/mail/MessageDetailPane.js +87 -36
- package/dist/apps/shared/components/mail/NewMailToasts.d.ts +19 -0
- package/dist/apps/shared/components/mail/NewMailToasts.js +55 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +30 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +71 -5
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +4 -3
- package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +1 -1
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +122 -27
- package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.d.ts +19 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.js +33 -0
- package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +9 -1
- package/dist/apps/shared/components/mail/compose/RichTextEditor.js +20 -2
- package/dist/apps/shared/components/mail/compose/SendFailureAlert.d.ts +14 -0
- package/dist/apps/shared/components/mail/compose/SendFailureAlert.js +11 -0
- package/dist/apps/shared/components/mail/compose/composePerf.d.ts +16 -0
- package/dist/apps/shared/components/mail/compose/composePerf.js +41 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +13 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.js +67 -11
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +19 -4
- package/dist/apps/shared/components/mail/layout/MailShell.js +85 -85
- package/dist/apps/shared/components/mail/unreadStyle.d.ts +45 -0
- package/dist/apps/shared/components/mail/unreadStyle.js +61 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +1 -1
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +15 -5
- package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +1 -1
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +15 -5
- package/dist/apps/shared/keyboard/GlobalShortcuts.d.ts +14 -0
- package/dist/apps/shared/keyboard/GlobalShortcuts.js +37 -0
- package/dist/apps/shared/keyboard/ShortcutProvider.d.ts +20 -0
- package/dist/apps/shared/keyboard/ShortcutProvider.js +52 -0
- package/dist/apps/shared/keyboard/ShortcutsDialog.d.ts +14 -0
- package/dist/apps/shared/keyboard/ShortcutsDialog.js +42 -0
- package/dist/apps/shared/keyboard/dispatch.d.ts +16 -0
- package/dist/apps/shared/keyboard/dispatch.js +109 -0
- package/dist/apps/shared/keyboard/format.d.ts +12 -0
- package/dist/apps/shared/keyboard/format.js +74 -0
- package/dist/apps/shared/keyboard/keymap.d.ts +294 -0
- package/dist/apps/shared/keyboard/keymap.js +84 -0
- package/dist/apps/shared/keyboard/match.d.ts +20 -0
- package/dist/apps/shared/keyboard/match.js +31 -0
- package/dist/apps/shared/keyboard/parse.d.ts +31 -0
- package/dist/apps/shared/keyboard/parse.js +107 -0
- package/dist/apps/shared/keyboard/platform.d.ts +15 -0
- package/dist/apps/shared/keyboard/platform.js +22 -0
- package/dist/apps/shared/keyboard/registry.d.ts +39 -0
- package/dist/apps/shared/keyboard/registry.js +33 -0
- package/dist/apps/shared/keyboard/targets.d.ts +14 -0
- package/dist/apps/shared/keyboard/targets.js +67 -0
- package/dist/apps/shared/keyboard/useShortcut.d.ts +19 -0
- package/dist/apps/shared/keyboard/useShortcut.js +35 -0
- package/dist/apps/shared/keyboard/useShortcutProps.d.ts +10 -0
- package/dist/apps/shared/keyboard/useShortcutProps.js +15 -0
- package/dist/apps/shared/mail/folderCounts.d.ts +78 -0
- package/dist/apps/shared/mail/folderCounts.js +212 -0
- package/dist/apps/shared/mail/listSnapshots.d.ts +46 -0
- package/dist/apps/shared/mail/listSnapshots.js +43 -0
- package/dist/apps/shared/mail/mergeFirstPage.d.ts +23 -0
- package/dist/apps/shared/mail/mergeFirstPage.js +30 -0
- package/dist/apps/shared/mail/messageReadState.d.ts +32 -0
- package/dist/apps/shared/mail/messageReadState.js +63 -0
- package/dist/apps/shared/mail/newMailNotifications.d.ts +62 -0
- package/dist/apps/shared/mail/newMailNotifications.js +138 -0
- package/dist/apps/shared/mail/useMailConnection.d.ts +55 -0
- package/dist/apps/shared/mail/useMailConnection.js +96 -0
- package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +55 -0
- package/dist/apps/shared/mail/useMailLiveUpdates.js +182 -0
- package/dist/apps/shared/mail/useMarkMessageRead.d.ts +12 -0
- package/dist/apps/shared/mail/useMarkMessageRead.js +44 -0
- package/dist/apps/shared/mail/useNewMailNotifications.d.ts +41 -0
- package/dist/apps/shared/mail/useNewMailNotifications.js +110 -0
- package/dist/apps/shared/mail/useUnreadTitle.d.ts +18 -0
- package/dist/apps/shared/mail/useUnreadTitle.js +31 -0
- package/dist/apps/shared/navigation/AppRouter.d.ts +52 -0
- package/dist/apps/shared/navigation/AppRouter.js +242 -0
- package/dist/apps/shared/navigation/appHrefs.d.ts +14 -0
- package/dist/apps/shared/navigation/appHrefs.js +20 -0
- package/dist/apps/shared/navigation/frameContext.d.ts +19 -0
- package/dist/apps/shared/navigation/frameContext.js +24 -0
- package/dist/apps/shared/navigation/idle.d.ts +14 -0
- package/dist/apps/shared/navigation/idle.js +43 -0
- package/dist/apps/shared/navigation/routerContext.d.ts +37 -0
- package/dist/apps/shared/navigation/routerContext.js +56 -0
- package/dist/apps/shared/navigation/routes.d.ts +32 -0
- package/dist/apps/shared/navigation/routes.js +37 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.js +17 -3
- package/dist/apps/shared/styles/app.css +28 -10
- package/dist/apps/www/_routedPage.d.ts +12 -0
- package/dist/apps/www/_routedPage.js +19 -0
- package/dist/apps/www/_routes.d.ts +11 -0
- package/dist/apps/www/_routes.js +29 -0
- package/dist/apps/www/calendar/index.d.ts +2 -2
- package/dist/apps/www/calendar/index.js +39 -8
- package/dist/apps/www/contacts/[uid].d.ts +3 -9
- package/dist/apps/www/contacts/[uid].js +6 -2
- package/dist/apps/www/contacts/index.d.ts +2 -2
- package/dist/apps/www/contacts/index.js +17 -4
- package/dist/apps/www/index.d.ts +2 -2
- package/dist/apps/www/index.js +361 -34
- package/dist/apps/www/messages/[uid].d.ts +3 -7
- package/dist/apps/www/messages/[uid].js +7 -4
- package/dist/apps/www/settings/auto-reply/index.d.ts +2 -2
- package/dist/apps/www/settings/auto-reply/index.js +3 -1
- package/dist/apps/www/settings/encryption/index.d.ts +2 -2
- package/dist/apps/www/settings/encryption/index.js +3 -1
- package/dist/apps/www/settings/filters/[uid].d.ts +2 -2
- package/dist/apps/www/settings/filters/[uid].js +3 -1
- package/dist/apps/www/settings/filters/index.d.ts +2 -2
- package/dist/apps/www/settings/filters/index.js +3 -1
- package/dist/apps/www/settings/filters/new/index.d.ts +2 -2
- package/dist/apps/www/settings/filters/new/index.js +6 -2
- package/dist/apps/www/settings/labels/index.d.ts +2 -2
- package/dist/apps/www/settings/labels/index.js +3 -1
- package/dist/apps/www/settings/privacy/index.d.ts +2 -2
- package/dist/apps/www/settings/privacy/index.js +3 -1
- package/dist/apps/www/settings/read-receipts/index.d.ts +2 -2
- package/dist/apps/www/settings/read-receipts/index.js +3 -1
- package/dist/apps/www/settings/sharing/index.d.ts +2 -2
- package/dist/apps/www/settings/sharing/index.js +3 -1
- package/dist/apps/www/settings/signatures/[uid].d.ts +2 -2
- package/dist/apps/www/settings/signatures/[uid].js +3 -1
- package/dist/apps/www/settings/signatures/index.d.ts +2 -2
- package/dist/apps/www/settings/signatures/index.js +3 -1
- package/dist/apps/www/settings/signatures/new/index.d.ts +2 -2
- package/dist/apps/www/settings/signatures/new/index.js +6 -2
- package/dist/apps/www/tasks/index.d.ts +2 -2
- package/dist/apps/www/tasks/index.js +15 -3
- package/package.json +2 -2
|
@@ -1,129 +1,134 @@
|
|
|
1
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
-
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
-
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const [
|
|
41
|
-
const [
|
|
42
|
-
const [
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { routedPage } from "../../../_routedPage.js";
|
|
6
|
+
import { useNavigate } from "../../../../shared/navigation/AppRouter.js";
|
|
7
|
+
import React, { FormEvent, useState } from "react";
|
|
8
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
9
|
+
import { createMailSignature, listMailSignatures } from "@rapidmx/react-shared/mail/mailSignaturesApi.js";
|
|
10
|
+
import { clearPreviousDefaults } from "../signatureDefaults.js";
|
|
11
|
+
import SettingsShell, { SettingsShellProps, useSettingsShell } from "../../../../shared/components/settings/layout/SettingsShell.js";
|
|
12
|
+
import RichTextEditor from "../../../../shared/components/mail/compose/RichTextEditor.js";
|
|
13
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
14
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
15
|
+
import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
|
|
16
|
+
|
|
17
|
+
const INPUT_CLASS =
|
|
18
|
+
"w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
19
|
+
|
|
20
|
+
export type NewSignaturePageProps = Omit<SettingsShellProps, "active">;
|
|
21
|
+
|
|
22
|
+
function NewSignaturePage(props: NewSignaturePageProps) {
|
|
23
|
+
return (
|
|
24
|
+
<SettingsShell {...props} active="signatures">
|
|
25
|
+
<NewSignatureForm />
|
|
26
|
+
</SettingsShell>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Signatures aren't backed by a draft/attachment record the way Compose's own inline images are (see
|
|
31
|
+
* `ComposeWindow.handleUploadImage()`) — inline images in a signature are out of scope for this phase
|
|
32
|
+
* (`.claude/NOTES.md`'s Phase 12 entry), so this is a real, deliberate no-op, not a stub left unfinished. */
|
|
33
|
+
async function handleUploadImage(): Promise<string | null> {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function NewSignatureForm() {
|
|
38
|
+
const navigate = useNavigate();
|
|
39
|
+
const { mailboxUid } = useSettingsShell();
|
|
40
|
+
const [name, setName] = useState("");
|
|
41
|
+
const [contentHtml, setContentHtml] = useState("");
|
|
42
|
+
const [isDefaultForNewMessages, setIsDefaultForNewMessages] = useState(false);
|
|
43
|
+
const [isDefaultForReplyForward, setIsDefaultForReplyForward] = useState(false);
|
|
44
|
+
const [error, setError] = useState<string | null>(null);
|
|
45
|
+
const [saving, setSaving] = useState(false);
|
|
46
|
+
|
|
47
|
+
async function handleSubmit(e: FormEvent) {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
setError(null);
|
|
50
|
+
|
|
51
|
+
if (!name.trim()) {
|
|
52
|
+
setError("A name is required.");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setSaving(true);
|
|
57
|
+
try {
|
|
58
|
+
if (isDefaultForNewMessages || isDefaultForReplyForward) {
|
|
59
|
+
const existing = await listMailSignatures(mailboxUid!, { limit: 100 });
|
|
60
|
+
await clearPreviousDefaults(existing, undefined, isDefaultForNewMessages, isDefaultForReplyForward);
|
|
61
|
+
}
|
|
62
|
+
const created = await createMailSignature({
|
|
63
|
+
mailboxUid: mailboxUid!,
|
|
64
|
+
name: name.trim(),
|
|
65
|
+
contentHtml,
|
|
66
|
+
isDefaultForNewMessages,
|
|
67
|
+
isDefaultForReplyForward,
|
|
68
|
+
});
|
|
69
|
+
navigate(`/settings/signatures/${encodeURIComponent(created.uid)}?mailboxUid=${encodeURIComponent(mailboxUid!)}`);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not create the signature.");
|
|
72
|
+
} finally {
|
|
73
|
+
setSaving(false);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="flex-1 min-w-0 overflow-y-auto p-6">
|
|
79
|
+
<div className="max-w-3xl">
|
|
80
|
+
<h1 className="text-lg font-bold tracking-tight mb-5">New signature</h1>
|
|
81
|
+
|
|
82
|
+
{error && <Alert>{error}</Alert>}
|
|
83
|
+
|
|
84
|
+
<form onSubmit={handleSubmit} className="flex flex-col gap-5">
|
|
85
|
+
<div className="bg-surface border border-border rounded-md p-6 flex flex-col gap-4">
|
|
86
|
+
<FormField label="Name" htmlFor="name">
|
|
87
|
+
<input
|
|
88
|
+
id="name"
|
|
89
|
+
type="text"
|
|
90
|
+
className={INPUT_CLASS}
|
|
91
|
+
value={name}
|
|
92
|
+
onChange={(e) => setName(e.target.value)}
|
|
93
|
+
placeholder="Work signature"
|
|
94
|
+
/>
|
|
95
|
+
</FormField>
|
|
96
|
+
<label className="flex items-center gap-2 text-sm">
|
|
97
|
+
<input
|
|
98
|
+
type="checkbox"
|
|
99
|
+
checked={isDefaultForNewMessages}
|
|
100
|
+
onChange={(e) => setIsDefaultForNewMessages(e.target.checked)}
|
|
101
|
+
/>
|
|
102
|
+
Use for new messages
|
|
103
|
+
</label>
|
|
104
|
+
<label className="flex items-center gap-2 text-sm">
|
|
105
|
+
<input
|
|
106
|
+
type="checkbox"
|
|
107
|
+
checked={isDefaultForReplyForward}
|
|
108
|
+
onChange={(e) => setIsDefaultForReplyForward(e.target.checked)}
|
|
109
|
+
/>
|
|
110
|
+
Use for replies and forwards
|
|
111
|
+
</label>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div className="h-64">
|
|
115
|
+
<RichTextEditor value={contentHtml} onChange={setContentHtml} fill onUploadImage={handleUploadImage} />
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div className="flex gap-3">
|
|
119
|
+
<Button type="submit" loading={saving} disabled={saving} className="!w-auto">
|
|
120
|
+
Create signature
|
|
121
|
+
</Button>
|
|
122
|
+
<a href={`/settings/signatures?mailboxUid=${encodeURIComponent(mailboxUid!)}`}>
|
|
123
|
+
<Button type="button" variant="secondary" className="!w-auto">
|
|
124
|
+
Cancel
|
|
125
|
+
</Button>
|
|
126
|
+
</a>
|
|
127
|
+
</div>
|
|
128
|
+
</form>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export default routedPage("/settings/signatures/new", NewSignaturePage);
|
package/apps/www/tasks/index.tsx
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
import
|
|
5
|
+
import { routedPage } from "../_routedPage.js";
|
|
6
|
+
import React, { FormEvent, useEffect, useMemo, useRef, useState } from "react";
|
|
6
7
|
import { endOfWeek, isAfter, isBefore, isToday, parseISO, startOfDay } from "date-fns";
|
|
7
8
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
9
|
import {
|
|
@@ -26,11 +27,14 @@ import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
|
26
27
|
import { findWellKnownFolderUid } from "../../shared/mail/findWellKnownFolderUid.js";
|
|
27
28
|
import { useWritableMailboxes } from "../../shared/components/mail/writableMailboxes.js";
|
|
28
29
|
import { LIST_PAGE_SIZE, MAX_LIST_PAGES, listAllPages } from "../../shared/mail/listAllPages.js";
|
|
30
|
+
import { SHORTCUTS } from "../../shared/keyboard/keymap.js";
|
|
31
|
+
import { useShortcut } from "../../shared/keyboard/useShortcut.js";
|
|
32
|
+
import { useShortcutProps } from "../../shared/keyboard/useShortcutProps.js";
|
|
29
33
|
|
|
30
34
|
const INPUT_CLASS =
|
|
31
35
|
"text-sm py-1.5 px-2 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
function TasksPage(props: TasksShellProps) {
|
|
34
38
|
return (
|
|
35
39
|
<TasksShell {...props}>
|
|
36
40
|
<TasksContent />
|
|
@@ -103,6 +107,14 @@ function TasksContent() {
|
|
|
103
107
|
// checked task (the toolbar's Delete).
|
|
104
108
|
const [pendingDelete, setPendingDelete] = useState<{ type: "single"; task: Task } | { type: "bulk" } | null>(null);
|
|
105
109
|
|
|
110
|
+
// "New task" from the keyboard: a task is made in the quick-add form, so this moves the caret to its title field (which the Flagged view,
|
|
111
|
+
// a list of flagged mail rather than tasks, doesn't have). A dialog open - the delete confirmation - silences it.
|
|
112
|
+
const addTaskRef = useRef<HTMLInputElement | null>(null);
|
|
113
|
+
const canAddTask = view.type !== "flagged";
|
|
114
|
+
// Registered only while the form is rendered (`canAddTask`), so its field is there.
|
|
115
|
+
useShortcut(SHORTCUTS.tasks.create, () => addTaskRef.current!.focus(), { enabled: canAddTask });
|
|
116
|
+
const addTaskHint = useShortcutProps("Add a task", SHORTCUTS.tasks.create, canAddTask);
|
|
117
|
+
|
|
106
118
|
function reload(): Promise<void> {
|
|
107
119
|
if (!folderUid) {
|
|
108
120
|
setTasks([]);
|
|
@@ -344,8 +356,10 @@ function TasksContent() {
|
|
|
344
356
|
<>
|
|
345
357
|
<form onSubmit={handleCreate} className="flex items-center gap-2 bg-surface border border-border rounded-md p-2">
|
|
346
358
|
<input
|
|
359
|
+
ref={addTaskRef}
|
|
347
360
|
type="text"
|
|
348
361
|
aria-label="Add a task"
|
|
362
|
+
{...addTaskHint}
|
|
349
363
|
className={`${INPUT_CLASS} flex-1`}
|
|
350
364
|
value={title}
|
|
351
365
|
onChange={(e) => setTitle(e.target.value)}
|
|
@@ -636,3 +650,5 @@ function TaskTable({ tasks, checkedUids, onToggleChecked, onToggle, onDelete }:
|
|
|
636
650
|
</div>
|
|
637
651
|
);
|
|
638
652
|
}
|
|
653
|
+
|
|
654
|
+
export default routedPage("/tasks", TasksPage);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
/** auth-server's account page, or `undefined` when no auth-server is configured. Tolerates a configured URL with a trailing slash. */
|
|
6
|
+
export function accountUrlOf(authServerUrl) {
|
|
7
|
+
return authServerUrl ? `${authServerUrl.replace(/\/+$/, "")}/account` : undefined;
|
|
8
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the signed-in user holds an administrator role, learned without elevating.
|
|
3
|
+
*
|
|
4
|
+
* The JWT the browser carries can't say: auth-server strips every trusted role from a token that isn't elevated
|
|
5
|
+
* (`TokenUtils.resolveTokenUser()`), so `trusted` (from the JWT) is false for a real administrator until they confirm
|
|
6
|
+
* their identity on auth-server's elevation page. The role itself lives on the caller's own `User` record, which they may
|
|
7
|
+
* always read - `GET {authServerUrl}/api/users/me` (the same call auth-server's own UI makes "to check for admin access") -
|
|
8
|
+
* so the answer is read from there, with the same cross-origin credentials the profile lookups use.
|
|
9
|
+
*
|
|
10
|
+
* This only decides whether a menu item is shown. The `/admin` pages and API stay authoritative: they check the role and
|
|
11
|
+
* the elevation themselves, and the link only navigates (`/admin` then runs the elevation redirect), so a wrong answer
|
|
12
|
+
* - a stale cache, a tampered `sessionStorage` - can show a link that leads to "no administrator access", never more.
|
|
13
|
+
*/
|
|
14
|
+
/** The roles the server treats as trusted when it isn't configured otherwise (`trusted_roles`, default `["admin"]`). */
|
|
15
|
+
export declare const DEFAULT_TRUSTED_ROLES: readonly string[];
|
|
16
|
+
/** `sessionStorage` key prefix of a remembered answer; the user's uid follows, so another account signing in in the same
|
|
17
|
+
* tab never inherits it. */
|
|
18
|
+
export declare const ADMIN_ACCESS_KEY_PREFIX = "rapidmx-admin-access:";
|
|
19
|
+
/** How long a remembered answer is trusted - long enough that navigating between the apps costs no request, short enough
|
|
20
|
+
* that a role granted or removed shows up within the sitting. */
|
|
21
|
+
export declare const ADMIN_ACCESS_TTL_MS: number;
|
|
22
|
+
/** Forgets the lookups in flight (tests). */
|
|
23
|
+
export declare function resetAdminAccessLookups(): void;
|
|
24
|
+
/**
|
|
25
|
+
* Resolves `true` when `userUid`'s own `User` record on auth-server carries one of `trustedRoles`, `false` when it
|
|
26
|
+
* doesn't, and `undefined` when that couldn't be established (unreachable, blocked by CORS, not signed in, an unexpected
|
|
27
|
+
* body, or a record that isn't this user's). Only a real answer is remembered; a failure is asked again on the next page.
|
|
28
|
+
* Never rejects.
|
|
29
|
+
*/
|
|
30
|
+
export declare function lookUpAdminAccess(authServerUrl: string, userUid: string, trustedRoles?: readonly string[], now?: number): Promise<boolean | undefined>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { authApiFetch } from "@rapidmx/react-shared/util/api.js";
|
|
6
|
+
/**
|
|
7
|
+
* Whether the signed-in user holds an administrator role, learned without elevating.
|
|
8
|
+
*
|
|
9
|
+
* The JWT the browser carries can't say: auth-server strips every trusted role from a token that isn't elevated
|
|
10
|
+
* (`TokenUtils.resolveTokenUser()`), so `trusted` (from the JWT) is false for a real administrator until they confirm
|
|
11
|
+
* their identity on auth-server's elevation page. The role itself lives on the caller's own `User` record, which they may
|
|
12
|
+
* always read - `GET {authServerUrl}/api/users/me` (the same call auth-server's own UI makes "to check for admin access") -
|
|
13
|
+
* so the answer is read from there, with the same cross-origin credentials the profile lookups use.
|
|
14
|
+
*
|
|
15
|
+
* This only decides whether a menu item is shown. The `/admin` pages and API stay authoritative: they check the role and
|
|
16
|
+
* the elevation themselves, and the link only navigates (`/admin` then runs the elevation redirect), so a wrong answer
|
|
17
|
+
* - a stale cache, a tampered `sessionStorage` - can show a link that leads to "no administrator access", never more.
|
|
18
|
+
*/
|
|
19
|
+
/** The roles the server treats as trusted when it isn't configured otherwise (`trusted_roles`, default `["admin"]`). */
|
|
20
|
+
export const DEFAULT_TRUSTED_ROLES = ["admin"];
|
|
21
|
+
/** `sessionStorage` key prefix of a remembered answer; the user's uid follows, so another account signing in in the same
|
|
22
|
+
* tab never inherits it. */
|
|
23
|
+
export const ADMIN_ACCESS_KEY_PREFIX = "rapidmx-admin-access:";
|
|
24
|
+
/** How long a remembered answer is trusted - long enough that navigating between the apps costs no request, short enough
|
|
25
|
+
* that a role granted or removed shows up within the sitting. */
|
|
26
|
+
export const ADMIN_ACCESS_TTL_MS = 30 * 60 * 1000;
|
|
27
|
+
/** Lookups in flight, by key, so two components asking at once make one request. */
|
|
28
|
+
const lookups = new Map();
|
|
29
|
+
/** Forgets the lookups in flight (tests). */
|
|
30
|
+
export function resetAdminAccessLookups() {
|
|
31
|
+
lookups.clear();
|
|
32
|
+
}
|
|
33
|
+
function readRemembered(key, now) {
|
|
34
|
+
try {
|
|
35
|
+
const stored = JSON.parse(sessionStorage.getItem(key) ?? "null");
|
|
36
|
+
if (typeof stored?.admin === "boolean" && typeof stored.at === "number" && now - stored.at >= 0 && now - stored.at < ADMIN_ACCESS_TTL_MS) {
|
|
37
|
+
return stored.admin;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Unreadable, or storage unavailable - ask again.
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
function remember(key, admin, now) {
|
|
46
|
+
try {
|
|
47
|
+
sessionStorage.setItem(key, JSON.stringify({ admin, at: now }));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Storage unavailable - the next page load asks again.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Resolves `true` when `userUid`'s own `User` record on auth-server carries one of `trustedRoles`, `false` when it
|
|
55
|
+
* doesn't, and `undefined` when that couldn't be established (unreachable, blocked by CORS, not signed in, an unexpected
|
|
56
|
+
* body, or a record that isn't this user's). Only a real answer is remembered; a failure is asked again on the next page.
|
|
57
|
+
* Never rejects.
|
|
58
|
+
*/
|
|
59
|
+
export function lookUpAdminAccess(authServerUrl, userUid, trustedRoles = DEFAULT_TRUSTED_ROLES, now = Date.now()) {
|
|
60
|
+
const key = `${ADMIN_ACCESS_KEY_PREFIX}${userUid}`;
|
|
61
|
+
const remembered = readRemembered(key, now);
|
|
62
|
+
if (remembered !== undefined) {
|
|
63
|
+
return Promise.resolve(remembered);
|
|
64
|
+
}
|
|
65
|
+
const inFlight = lookups.get(key);
|
|
66
|
+
if (inFlight) {
|
|
67
|
+
return inFlight;
|
|
68
|
+
}
|
|
69
|
+
const lookup = (async () => {
|
|
70
|
+
try {
|
|
71
|
+
const user = await authApiFetch(authServerUrl, "/users/me");
|
|
72
|
+
if (!user || user.uid !== userUid || !Array.isArray(user.roles)) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const admin = user.roles.some((role) => typeof role === "string" && trustedRoles.includes(role));
|
|
76
|
+
remember(key, admin, now);
|
|
77
|
+
return admin;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
// Answered: what was learned is in storage; a failure may be asked again.
|
|
84
|
+
lookups.delete(key);
|
|
85
|
+
}
|
|
86
|
+
})();
|
|
87
|
+
lookups.set(key, lookup);
|
|
88
|
+
return lookup;
|
|
89
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `ApiRequestError.code` a `@RequiresElevation()` endpoint answers a caller whose token isn't elevated with
|
|
3
|
+
* (a 403 - `ApiErrors.AUTH_REQUIRES_ELEVATION` in `@rapidrest/service-core`). Distinct from `api-103` (the caller is
|
|
4
|
+
* elevated but lacks the trusted role), which no amount of elevating fixes.
|
|
5
|
+
*/
|
|
6
|
+
export declare const ELEVATION_REQUIRED_CODE = "api-104";
|
|
7
|
+
/** `sessionStorage` key holding the time (`Date.now()`) the browser was last sent to auth-server to elevate. */
|
|
8
|
+
export declare const ELEVATION_ATTEMPT_KEY = "rapidmx-admin-elevation-attempt";
|
|
9
|
+
/** How long after sending the browser to elevate a second `api-104` is taken to mean the elevation didn't take
|
|
10
|
+
* effect (e.g. auth-server's elevated cookie never reaching this origin), rather than a fresh need to elevate. */
|
|
11
|
+
export declare const ELEVATION_RETRY_WINDOW_MS: number;
|
|
12
|
+
/** Whether `err` is a 403 from an elevation-gated endpoint for a caller whose token isn't elevated. */
|
|
13
|
+
export declare function isElevationRequired(err: unknown): boolean;
|
|
14
|
+
/** auth-server's elevation page: it sends the browser back to `returnTo` once the user has confirmed their
|
|
15
|
+
* identity, or to its own account page if they cancel. */
|
|
16
|
+
export declare function elevationUrl(authServerUrl: string, returnTo: string): string;
|
|
17
|
+
/** Whether the browser was sent to elevate within `ELEVATION_RETRY_WINDOW_MS` of `now`. `false` when nothing was
|
|
18
|
+
* recorded, the record is unreadable or from the future, or storage is unavailable. */
|
|
19
|
+
export declare function elevationAttemptedRecently(now?: number): boolean;
|
|
20
|
+
/** Remembers, for this tab, that the browser is about to be sent to elevate. A no-op where storage is unavailable
|
|
21
|
+
* (then only the first-round protection is lost - elevating needs the user to confirm each time round). */
|
|
22
|
+
export declare function recordElevationAttempt(now?: number): void;
|
|
23
|
+
/** Forgets any recorded attempt - once elevation has worked, or when the user asks to try again. */
|
|
24
|
+
export declare function clearElevationAttempt(): void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
6
|
+
/**
|
|
7
|
+
* The `ApiRequestError.code` a `@RequiresElevation()` endpoint answers a caller whose token isn't elevated with
|
|
8
|
+
* (a 403 - `ApiErrors.AUTH_REQUIRES_ELEVATION` in `@rapidrest/service-core`). Distinct from `api-103` (the caller is
|
|
9
|
+
* elevated but lacks the trusted role), which no amount of elevating fixes.
|
|
10
|
+
*/
|
|
11
|
+
export const ELEVATION_REQUIRED_CODE = "api-104";
|
|
12
|
+
/** `sessionStorage` key holding the time (`Date.now()`) the browser was last sent to auth-server to elevate. */
|
|
13
|
+
export const ELEVATION_ATTEMPT_KEY = "rapidmx-admin-elevation-attempt";
|
|
14
|
+
/** How long after sending the browser to elevate a second `api-104` is taken to mean the elevation didn't take
|
|
15
|
+
* effect (e.g. auth-server's elevated cookie never reaching this origin), rather than a fresh need to elevate. */
|
|
16
|
+
export const ELEVATION_RETRY_WINDOW_MS = 2 * 60 * 1000;
|
|
17
|
+
/** Whether `err` is a 403 from an elevation-gated endpoint for a caller whose token isn't elevated. */
|
|
18
|
+
export function isElevationRequired(err) {
|
|
19
|
+
return err instanceof ApiRequestError && err.status === 403 && err.code === ELEVATION_REQUIRED_CODE;
|
|
20
|
+
}
|
|
21
|
+
/** auth-server's elevation page: it sends the browser back to `returnTo` once the user has confirmed their
|
|
22
|
+
* identity, or to its own account page if they cancel. */
|
|
23
|
+
export function elevationUrl(authServerUrl, returnTo) {
|
|
24
|
+
return `${authServerUrl}/auth/elevate?return_to=${encodeURIComponent(returnTo)}`;
|
|
25
|
+
}
|
|
26
|
+
/** Whether the browser was sent to elevate within `ELEVATION_RETRY_WINDOW_MS` of `now`. `false` when nothing was
|
|
27
|
+
* recorded, the record is unreadable or from the future, or storage is unavailable. */
|
|
28
|
+
export function elevationAttemptedRecently(now = Date.now()) {
|
|
29
|
+
try {
|
|
30
|
+
const recorded = Number(sessionStorage.getItem(ELEVATION_ATTEMPT_KEY));
|
|
31
|
+
const elapsed = now - recorded;
|
|
32
|
+
return recorded > 0 && elapsed >= 0 && elapsed < ELEVATION_RETRY_WINDOW_MS;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Remembers, for this tab, that the browser is about to be sent to elevate. A no-op where storage is unavailable
|
|
39
|
+
* (then only the first-round protection is lost - elevating needs the user to confirm each time round). */
|
|
40
|
+
export function recordElevationAttempt(now = Date.now()) {
|
|
41
|
+
try {
|
|
42
|
+
sessionStorage.setItem(ELEVATION_ATTEMPT_KEY, String(now));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// See the doc comment.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Forgets any recorded attempt - once elevation has worked, or when the user asks to try again. */
|
|
49
|
+
export function clearElevationAttempt() {
|
|
50
|
+
try {
|
|
51
|
+
sessionStorage.removeItem(ELEVATION_ATTEMPT_KEY);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Nothing was recorded.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -24,9 +24,17 @@ export interface AdminShellProps extends PluginNavProps {
|
|
|
24
24
|
* off-rail sections' - see `mergePluginNavItems`. */
|
|
25
25
|
export declare function adminNavItems(pluginNav?: PluginNav): NavItem[];
|
|
26
26
|
/**
|
|
27
|
-
* Gates every `apps/admin` page behind the `admin` trusted role
|
|
28
|
-
* `BaseAdminRoute` endpoint works — this one is side-effect-free) purely as a
|
|
29
|
-
*
|
|
30
|
-
*
|
|
27
|
+
* Gates every `apps/admin` page behind the `admin` trusted role, and behind an elevated session. Uses
|
|
28
|
+
* `GET /api/admin/release-notes` (any `BaseAdminRoute` endpoint works — this one is side-effect-free) purely as a
|
|
29
|
+
* canary. The endpoint is class-level `@RequiresElevation()`, checked *before* the trusted-role check, so a 200 means the
|
|
30
|
+
* caller's JWT is elevated and carries a trusted role: show the console. Otherwise:
|
|
31
|
+
*
|
|
32
|
+
* 403 `api-104` means the JWT isn't elevated (an administrator's normal sign-in). There is no local step-up form; the
|
|
33
|
+
* browser is sent to auth-server's `/auth/elevate?return_to=<this page>`, which returns it here once the user has
|
|
34
|
+
* confirmed their identity (or to its own account page if they cancel). Sent at most once per
|
|
35
|
+
* `ELEVATION_RETRY_WINDOW_MS`, so an elevated cookie that never reaches this origin can't bounce the browser back
|
|
36
|
+
* and forth - see `elevation.ts`, and the "didn't take effect" alert with its own "Try again" below.
|
|
37
|
+
*
|
|
38
|
+
* 403 `api-103` (elevated, but not an administrator), any other 403, and 401 mean "no administrator access".
|
|
31
39
|
*/
|
|
32
40
|
export default function AdminShell({ active, userUid, authServerUrl, pluginNav, children }: PropsWithChildren<AdminShellProps>): React.JSX.Element;
|