@rebasepro/app 0.17.3 → 0.18.1
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 +4 -1
- package/dist/components/index.d.ts +0 -3
- package/dist/contexts/AuthControllerContext.d.ts +2 -1
- package/dist/contexts/CustomizationControllerContext.d.ts +2 -1
- package/dist/contexts/DialogsProvider.d.ts +2 -1
- package/dist/contexts/RebaseDataContext.d.ts +10 -1
- package/dist/contexts/StorageSourceContext.d.ts +11 -1
- package/dist/core/RebaseProps.d.ts +2 -12
- package/dist/debug/index.d.ts +19 -0
- package/dist/debug.js +3631 -0
- package/dist/hooks/data/delete.d.ts +16 -7
- package/dist/hooks/data/save.d.ts +18 -4
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/useAdminModeController.d.ts +5 -4
- package/dist/hooks/useNavigationGroupLabel.d.ts +17 -0
- package/dist/hooks/useStudioBridge.d.ts +33 -0
- package/dist/index.es.js +2599 -6980
- package/dist/index.es.js.map +1 -1
- package/dist/vitePlugin.d.ts +22 -0
- package/dist/vitePlugin.js +40 -1
- package/package.json +42 -31
- package/src/auth/api.ts +0 -134
- package/src/auth/index.ts +0 -26
- package/src/auth/types.ts +0 -115
- package/src/auth/useRebaseAuthController.ts +0 -345
- package/src/collections/collection_view_config.ts +0 -76
- package/src/collections/entity-display-cache.ts +0 -195
- package/src/collections/entity-display.ts +0 -77
- package/src/collections/entity_image_preview.ts +0 -61
- package/src/collections/filter-operator-resolution.ts +0 -119
- package/src/collections/form-layout.ts +0 -394
- package/src/collections/index.ts +0 -13
- package/src/collections/navigation_from_path.ts +0 -123
- package/src/collections/navigation_utils.ts +0 -171
- package/src/collections/parent_references_from_path.ts +0 -59
- package/src/collections/property-path.ts +0 -30
- package/src/collections/property_presentation.ts +0 -264
- package/src/collections/summary-property.ts +0 -114
- package/src/collections/title-property.ts +0 -312
- package/src/components/AIIcon.tsx +0 -47
- package/src/components/AuthSimulationMenu.tsx +0 -0
- package/src/components/ConfirmationDialog.tsx +0 -48
- package/src/components/Debug/UIReferenceView.tsx +0 -1053
- package/src/components/Debug/UIStyleGuide.tsx +0 -159
- package/src/components/Debug/collection-views/CardViewDemo.tsx +0 -87
- package/src/components/Debug/collection-views/CollectionTableDemo.tsx +0 -107
- package/src/components/Debug/collection-views/KanbanBoardDemo.tsx +0 -89
- package/src/components/Debug/collection-views/index.ts +0 -3
- package/src/components/Debug/collection-views/sample_data.ts +0 -44
- package/src/components/Debug/crm-dashboard/CalendarWidget.tsx +0 -387
- package/src/components/Debug/crm-dashboard/CrmDashboardDemo.tsx +0 -1100
- package/src/components/Debug/crm-dashboard/DashboardMetrics.tsx +0 -131
- package/src/components/Debug/crm-dashboard/PipelineOverview.tsx +0 -288
- package/src/components/Debug/crm-dashboard/RecentActivity.tsx +0 -213
- package/src/components/Debug/crm-dashboard/TaskFilters.tsx +0 -182
- package/src/components/Debug/crm-dashboard/TaskTable.tsx +0 -425
- package/src/components/Debug/crm-dashboard/TasksView.tsx +0 -234
- package/src/components/ErrorTooltip.tsx +0 -12
- package/src/components/ErrorView.tsx +0 -94
- package/src/components/LanguageToggle.tsx +0 -59
- package/src/components/LoginView/LoginView.tsx +0 -1235
- package/src/components/LoginView/index.ts +0 -2
- package/src/components/LoginView/oauth-redirect-flow.ts +0 -184
- package/src/components/NotFoundPage.tsx +0 -27
- package/src/components/RebaseAuth.tsx +0 -27
- package/src/components/RebaseLogo.tsx +0 -88
- package/src/components/SchemaDriftBanner.tsx +0 -102
- package/src/components/UnsavedChangesDialog.tsx +0 -46
- package/src/components/UserDisplay.tsx +0 -54
- package/src/components/UserSelectPopover.tsx +0 -399
- package/src/components/UserSettingsView.tsx +0 -318
- package/src/components/common/index.ts +0 -6
- package/src/components/common/table_height.tsx +0 -21
- package/src/components/common/types.tsx +0 -66
- package/src/components/common/useColumnsIds.tsx +0 -261
- package/src/components/common/useDataTableController.tsx +0 -571
- package/src/components/common/useDebouncedData.ts +0 -49
- package/src/components/common/useScrollRestoration.tsx +0 -68
- package/src/components/index.tsx +0 -26
- package/src/contexts/AdminModeController.tsx +0 -11
- package/src/contexts/AnalyticsContext.tsx +0 -4
- package/src/contexts/AuthControllerContext.tsx +0 -4
- package/src/contexts/CollectionResolverContext.tsx +0 -24
- package/src/contexts/CollectionScopeContext.tsx +0 -68
- package/src/contexts/ComponentOverrideContext.tsx +0 -81
- package/src/contexts/CustomizationControllerContext.tsx +0 -4
- package/src/contexts/DataDriverContext.tsx +0 -4
- package/src/contexts/DataSourcesContext.tsx +0 -36
- package/src/contexts/DatabaseAdminContext.tsx +0 -4
- package/src/contexts/DialogsProvider.tsx +0 -56
- package/src/contexts/EffectiveRoleController.tsx +0 -12
- package/src/contexts/ModeController.tsx +0 -11
- package/src/contexts/RebaseClientInstanceContext.tsx +0 -7
- package/src/contexts/RebaseDataContext.tsx +0 -4
- package/src/contexts/SnackbarProvider.tsx +0 -15
- package/src/contexts/StorageSourceContext.tsx +0 -4
- package/src/contexts/StorageSourcesContext.tsx +0 -30
- package/src/contexts/UserConfigurationPersistenceContext.tsx +0 -4
- package/src/contexts/index.ts +0 -18
- package/src/core/PluginLifecycleManager.tsx +0 -96
- package/src/core/PluginProviderStack.tsx +0 -40
- package/src/core/Rebase.tsx +0 -488
- package/src/core/RebaseProps.tsx +0 -333
- package/src/core/RebaseRouter.tsx +0 -18
- package/src/core/RebaseRoutes.tsx +0 -24
- package/src/core/index.tsx +0 -5
- package/src/hooks/ApiConfigContext.tsx +0 -83
- package/src/hooks/data/collectionQuery.ts +0 -55
- package/src/hooks/data/delete.ts +0 -56
- package/src/hooks/data/save.ts +0 -78
- package/src/hooks/data/useCollection.tsx +0 -183
- package/src/hooks/data/useData.tsx +0 -18
- package/src/hooks/data/useDataOrder.ts +0 -26
- package/src/hooks/data/useFetch.tsx +0 -156
- package/src/hooks/data/useRelationSelector.tsx +0 -280
- package/src/hooks/index.tsx +0 -45
- package/src/hooks/useAdminModeController.tsx +0 -23
- package/src/hooks/useAnalyticsController.tsx +0 -8
- package/src/hooks/useAuthController.tsx +0 -15
- package/src/hooks/useAuthSubscription.ts +0 -130
- package/src/hooks/useBackendStorageSource.ts +0 -295
- package/src/hooks/useBridgeRegistration.tsx +0 -32
- package/src/hooks/useBrowserTitleAndIcon.tsx +0 -23
- package/src/hooks/useBuildAdminModeController.tsx +0 -29
- package/src/hooks/useBuildEffectiveRoleController.tsx +0 -30
- package/src/hooks/useBuildLocalConfigurationPersistence.tsx +0 -83
- package/src/hooks/useBuildModeController.tsx +0 -73
- package/src/hooks/useClipboard.tsx +0 -158
- package/src/hooks/useCollapsedGroups.ts +0 -116
- package/src/hooks/useComponentOverride.tsx +0 -59
- package/src/hooks/useCustomizationController.tsx +0 -14
- package/src/hooks/useDialogsController.tsx +0 -15
- package/src/hooks/useEffectiveRoleController.tsx +0 -10
- package/src/hooks/useLargeLayout.tsx +0 -65
- package/src/hooks/useModeController.tsx +0 -23
- package/src/hooks/useNavigationBlocker.tsx +0 -108
- package/src/hooks/usePermissions.ts +0 -45
- package/src/hooks/useRebaseClient.tsx +0 -10
- package/src/hooks/useRebaseContext.tsx +0 -84
- package/src/hooks/useRebaseRegistry.tsx +0 -91
- package/src/hooks/useResolvedComponent.tsx +0 -195
- package/src/hooks/useSlot.tsx +0 -64
- package/src/hooks/useSnackbarController.tsx +0 -80
- package/src/hooks/useStorageSource.tsx +0 -11
- package/src/hooks/useStudioBridge.tsx +0 -258
- package/src/hooks/useTranslation.ts +0 -38
- package/src/hooks/useUnsavedChangesDialog.tsx +0 -67
- package/src/hooks/useUserConfigurationPersistence.tsx +0 -11
- package/src/i18n/RebaseI18nProvider.tsx +0 -159
- package/src/index.ts +0 -32
- package/src/internal/common.tsx +0 -28
- package/src/internal/useRestoreScroll.tsx +0 -69
- package/src/locales/de.ts +0 -941
- package/src/locales/en.ts +0 -1021
- package/src/locales/es.ts +0 -962
- package/src/locales/fr.ts +0 -941
- package/src/locales/hi.ts +0 -941
- package/src/locales/it.ts +0 -941
- package/src/locales/pt.ts +0 -949
- package/src/util/constants.ts +0 -7
- package/src/util/createFormexStub.tsx +0 -66
- package/src/util/entity_cache.ts +0 -242
- package/src/util/enums.ts +0 -34
- package/src/util/icon_list.ts +0 -22
- package/src/util/icons.tsx +0 -129
- package/src/util/index.ts +0 -13
- package/src/util/local_storage.ts +0 -66
- package/src/util/previews.ts +0 -127
- package/src/util/useStorageUploadController.tsx +0 -354
- package/src/vitePlugin.ts +0 -215
- /package/dist/{components/Debug → debug}/UIReferenceView.d.ts +0 -0
- /package/dist/{components/Debug → debug}/UIStyleGuide.d.ts +0 -0
- /package/dist/{components/Debug → debug}/collection-views/CardViewDemo.d.ts +0 -0
- /package/dist/{components/Debug → debug}/collection-views/CollectionTableDemo.d.ts +0 -0
- /package/dist/{components/Debug → debug}/collection-views/KanbanBoardDemo.d.ts +0 -0
- /package/dist/{components/Debug → debug}/collection-views/index.d.ts +0 -0
- /package/dist/{components/Debug → debug}/collection-views/sample_data.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/CalendarWidget.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/CrmDashboardDemo.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/DashboardMetrics.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/PipelineOverview.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/RecentActivity.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/TaskFilters.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/TaskTable.d.ts +0 -0
- /package/dist/{components/Debug → debug}/crm-dashboard/TasksView.d.ts +0 -0
|
@@ -1,1235 +0,0 @@
|
|
|
1
|
-
import React, { ReactNode, useCallback, useEffect, useId, useRef, useState } from "react";
|
|
2
|
-
|
|
3
|
-
/** Google Identity Services SDK — injected by the GIS <script> tag. */
|
|
4
|
-
declare global {
|
|
5
|
-
interface Window {
|
|
6
|
-
google?: {
|
|
7
|
-
accounts: {
|
|
8
|
-
oauth2: {
|
|
9
|
-
initCodeClient(config: {
|
|
10
|
-
client_id: string;
|
|
11
|
-
scope: string;
|
|
12
|
-
ux_mode: "popup" | "redirect";
|
|
13
|
-
callback: (response: { code?: string; error?: string }) => void;
|
|
14
|
-
}): { requestCode(): void };
|
|
15
|
-
};
|
|
16
|
-
};
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
import {
|
|
22
|
-
ArrowLeftIcon,
|
|
23
|
-
Button,
|
|
24
|
-
Checkbox,
|
|
25
|
-
cls,
|
|
26
|
-
IconButton,
|
|
27
|
-
iconSize,
|
|
28
|
-
LoadingButton,
|
|
29
|
-
MailIcon,
|
|
30
|
-
Menu,
|
|
31
|
-
MenuItem,
|
|
32
|
-
MoonIcon,
|
|
33
|
-
SunIcon,
|
|
34
|
-
SunMoonIcon,
|
|
35
|
-
TextField,
|
|
36
|
-
Typography
|
|
37
|
-
} from "@rebasepro/ui";
|
|
38
|
-
import { User } from "@rebasepro/types";
|
|
39
|
-
import { AuthControllerExtended } from "@rebasepro/cms-types";
|
|
40
|
-
import { ErrorView } from "../ErrorView";
|
|
41
|
-
import { RebaseLogo } from "../RebaseLogo";
|
|
42
|
-
import { LanguageToggle } from "../LanguageToggle";
|
|
43
|
-
import { useModeController, useTranslation } from "../../hooks";
|
|
44
|
-
import { consumeOAuthCallback, startOAuthRedirect } from "./oauth-redirect-flow";
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Props for the generic LoginView.
|
|
48
|
-
* Consumes {@link AuthControllerExtended} — works with any backend
|
|
49
|
-
* (Rebase custom backend, Firebase, etc.)
|
|
50
|
-
*/
|
|
51
|
-
export interface LoginViewProps {
|
|
52
|
-
/**
|
|
53
|
-
* Auth controller — must implement AuthControllerExtended.
|
|
54
|
-
*/
|
|
55
|
-
authController: AuthControllerExtended;
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Path to the logo displayed in the login screen
|
|
59
|
-
*/
|
|
60
|
-
logo?: string;
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Enable the skip login button
|
|
64
|
-
*/
|
|
65
|
-
allowSkipLogin?: boolean;
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Disable the login buttons
|
|
69
|
-
*/
|
|
70
|
-
disabled?: boolean;
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Prevent users from creating new accounts.
|
|
74
|
-
* If not set, checks `authController.capabilities.registrationEnabled`.
|
|
75
|
-
*/
|
|
76
|
-
disableSignupScreen?: boolean;
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Display this component when no user is found
|
|
80
|
-
*/
|
|
81
|
-
noUserComponent?: ReactNode;
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Display this component below the sign-in buttons
|
|
85
|
-
*/
|
|
86
|
-
additionalComponent?: ReactNode;
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Display this component above the sign-in buttons
|
|
90
|
-
*/
|
|
91
|
-
topComponent?: ReactNode;
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Error message when user is not allowed access
|
|
95
|
-
*/
|
|
96
|
-
notAllowedError?: string | Error;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Google client ID for Google OAuth.
|
|
101
|
-
* Required when Google login is enabled via ID token flow.
|
|
102
|
-
*/
|
|
103
|
-
googleClientId?: string;
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* GitHub client ID for GitHub OAuth.
|
|
107
|
-
*/
|
|
108
|
-
githubClientId?: string;
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* LinkedIn client ID for LinkedIn OAuth.
|
|
112
|
-
*/
|
|
113
|
-
linkedinClientId?: string;
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Optional custom title shown above options
|
|
117
|
-
*/
|
|
118
|
-
title?: string;
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Optional custom subtitle shown above options
|
|
122
|
-
*/
|
|
123
|
-
subtitle?: string;
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* When true, shows bootstrap/setup UI (first-user creation).
|
|
127
|
-
* If not set, derived from `authController` if it exposes `needsSetup`.
|
|
128
|
-
*/
|
|
129
|
-
needsSetup?: boolean;
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Whether registration is enabled.
|
|
133
|
-
* If not set, derived from `authController.capabilities.registrationEnabled`.
|
|
134
|
-
*/
|
|
135
|
-
registrationEnabled?: boolean;
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Pre-fill the email field (e.g. for demo or testing environments).
|
|
139
|
-
*/
|
|
140
|
-
defaultEmail?: string;
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Pre-fill the password field (e.g. for demo or testing environments).
|
|
144
|
-
*/
|
|
145
|
-
defaultPassword?: string;
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* When set, a "Join the newsletter" opt-in checkbox is offered. Called with
|
|
149
|
-
* the address that just authenticated — by any method — if the box was
|
|
150
|
-
* ticked. Wire it to whatever stores the subscription; failures are the
|
|
151
|
-
* callback's problem, the login flow never waits on it.
|
|
152
|
-
*
|
|
153
|
-
* The checkbox sits on the **provider screen**, beside the buttons that
|
|
154
|
-
* choose how to sign in, rather than on the credentials form. It is a
|
|
155
|
-
* decision about the visitor, not about the password: asking on the
|
|
156
|
-
* credentials form put it in front of only the people who got that far by
|
|
157
|
-
* typing an address, so anyone signing in with Google was never offered it
|
|
158
|
-
* at all. Bootstrap mode is the exception — there is no provider screen
|
|
159
|
-
* there, so the form keeps it.
|
|
160
|
-
*/
|
|
161
|
-
onNewsletterOptIn?: (email: string) => void;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
type AuthMode = "buttons" | "login" | "register" | "forgot";
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* The shared field background mixin (`dark:bg-black/30`) is invisible on the
|
|
168
|
-
* near-black login card, so login inputs get an explicit border in dark mode.
|
|
169
|
-
*/
|
|
170
|
-
const loginFieldClasses = "w-full dark:border dark:border-surface-700/70 dark:hover:border-surface-600";
|
|
171
|
-
|
|
172
|
-
/** One warning per provider per page load — this is a config hint, not a log stream. */
|
|
173
|
-
const warnedProviders = new Set<string>();
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* An OAuth button needs a client id in the frontend *and* the provider
|
|
177
|
-
* configured on the backend. Half a configuration renders nothing at all, with
|
|
178
|
-
* no error anywhere — the button simply is not there, which reads as "the
|
|
179
|
-
* feature broke" rather than "one env var is missing". Say which half is
|
|
180
|
-
* missing instead.
|
|
181
|
-
*/
|
|
182
|
-
function warnAboutHalfConfiguredOAuth(
|
|
183
|
-
provider: string,
|
|
184
|
-
clientId: string | undefined,
|
|
185
|
-
backendProviders: string[]
|
|
186
|
-
) {
|
|
187
|
-
const backendHasIt = backendProviders.includes(provider);
|
|
188
|
-
if (Boolean(clientId) === backendHasIt) return; // both or neither — nothing to say
|
|
189
|
-
if (warnedProviders.has(provider)) return;
|
|
190
|
-
warnedProviders.add(provider);
|
|
191
|
-
|
|
192
|
-
const envVar = `${provider.toUpperCase()}_CLIENT_ID`;
|
|
193
|
-
console.warn(
|
|
194
|
-
clientId
|
|
195
|
-
? `[Rebase] ${provider} login is hidden: the login view has a client id, but the backend reports no "${provider}" provider. `
|
|
196
|
-
+ `Set ${envVar} (and ${provider.toUpperCase()}_CLIENT_SECRET) on the server and restart it.`
|
|
197
|
-
: `[Rebase] ${provider} login is hidden: the backend has a "${provider}" provider, but the login view was given no client id. `
|
|
198
|
-
+ `Pass it to the login view (e.g. VITE_${envVar}) and restart the frontend so the new env var is picked up.`
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Whether a provider's error code means "the visitor backed out" rather than
|
|
204
|
-
* "the sign-in failed". Closing the popup is an answer, not a fault, and
|
|
205
|
-
* showing it in red reads as a broken login.
|
|
206
|
-
*/
|
|
207
|
-
function isUserCancellation(code: string | undefined): boolean {
|
|
208
|
-
if (!code) return false;
|
|
209
|
-
return [
|
|
210
|
-
"access_denied",
|
|
211
|
-
"popup_closed",
|
|
212
|
-
"popup_closed_by_user",
|
|
213
|
-
"user_cancel",
|
|
214
|
-
"immediate_failed"
|
|
215
|
-
].includes(code);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Generic login view component that works with any AuthControllerExtended.
|
|
220
|
-
* Feature-detects capabilities to show/hide login methods.
|
|
221
|
-
* @group Core
|
|
222
|
-
*/
|
|
223
|
-
export function LoginView({
|
|
224
|
-
logo,
|
|
225
|
-
authController,
|
|
226
|
-
noUserComponent,
|
|
227
|
-
disableSignupScreen = false,
|
|
228
|
-
disabled = false,
|
|
229
|
-
notAllowedError,
|
|
230
|
-
googleClientId,
|
|
231
|
-
githubClientId,
|
|
232
|
-
linkedinClientId,
|
|
233
|
-
title,
|
|
234
|
-
subtitle,
|
|
235
|
-
needsSetup,
|
|
236
|
-
registrationEnabled,
|
|
237
|
-
additionalComponent,
|
|
238
|
-
topComponent,
|
|
239
|
-
defaultEmail,
|
|
240
|
-
defaultPassword,
|
|
241
|
-
onNewsletterOptIn
|
|
242
|
-
}: LoginViewProps) {
|
|
243
|
-
|
|
244
|
-
const modeState = useModeController();
|
|
245
|
-
const { mode: colorMode, setMode: setColorMode } = modeState;
|
|
246
|
-
const { t } = useTranslation();
|
|
247
|
-
|
|
248
|
-
const [mode, setMode] = useState<AuthMode>("buttons");
|
|
249
|
-
const [fadeIn, setFadeIn] = useState(false);
|
|
250
|
-
const [viewVisible, setViewVisible] = useState(true);
|
|
251
|
-
// Failures that never reach the auth controller: a provider that answers the
|
|
252
|
-
// popup with an error, a redirect whose `state` does not match, a Google
|
|
253
|
-
// script that never loaded. The controller records everything it is *given*
|
|
254
|
-
// in `authProviderError`, so it cannot speak for the half of the flow that
|
|
255
|
-
// happens before the code reaches it.
|
|
256
|
-
const [providerError, setProviderError] = useState<string | null>(null);
|
|
257
|
-
// Lives here, not in LoginForm: the box is ticked on the provider screen
|
|
258
|
-
// and read after a sign-in that happens one or two screens later, so it has
|
|
259
|
-
// to outlive both. The form also unmounts on every login↔register switch.
|
|
260
|
-
const [newsletterOptIn, setNewsletterOptIn] = useState(false);
|
|
261
|
-
|
|
262
|
-
// The controller as of the latest render. A provider callback resolves long
|
|
263
|
-
// after the click that started it, and the `authController` captured in that
|
|
264
|
-
// closure is the one from the render that mounted the button — which, for a
|
|
265
|
-
// controller replaced on each render, has no `user` on it yet.
|
|
266
|
-
const authControllerRef = useRef(authController);
|
|
267
|
-
authControllerRef.current = authController;
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Subscribe the address that just authenticated, if the visitor asked for
|
|
271
|
-
* it. Every sign-in path calls this, which is the point: the checkbox sits
|
|
272
|
-
* beside the provider buttons, so it has to mean the same thing whichever
|
|
273
|
-
* one is pressed.
|
|
274
|
-
*
|
|
275
|
-
* Only ever from a resolution path — a ticked box on a *failed* attempt
|
|
276
|
-
* must not subscribe an address nobody proved they control.
|
|
277
|
-
*/
|
|
278
|
-
const subscribeIfOptedIn = useCallback((email?: string | null) => {
|
|
279
|
-
if (!newsletterOptIn || !onNewsletterOptIn || !email) return;
|
|
280
|
-
onNewsletterOptIn(email);
|
|
281
|
-
}, [newsletterOptIn, onNewsletterOptIn]);
|
|
282
|
-
|
|
283
|
-
const switchMode = (newMode: AuthMode) => {
|
|
284
|
-
// A message about the last Google attempt has nothing to say about the
|
|
285
|
-
// email form the visitor just opened.
|
|
286
|
-
setProviderError(null);
|
|
287
|
-
setViewVisible(false);
|
|
288
|
-
setTimeout(() => {
|
|
289
|
-
setMode(newMode);
|
|
290
|
-
setViewVisible(true);
|
|
291
|
-
}, 150);
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
// Resolve capabilities — explicit props override authController.capabilities
|
|
295
|
-
const caps = authController.capabilities ?? {};
|
|
296
|
-
const isBootstrapMode = needsSetup
|
|
297
|
-
?? ("needsSetup" in authController && !!(authController as { needsSetup?: boolean }).needsSetup)
|
|
298
|
-
?? false;
|
|
299
|
-
const canRegister = registrationEnabled ?? caps.registrationEnabled ?? false;
|
|
300
|
-
// Every OAuth button needs BOTH halves: a client id here and the matching
|
|
301
|
-
// provider configured on the backend. `enabledProviders` is always an array
|
|
302
|
-
// (the controller defaults it to `[]`), so a missing entry means "the
|
|
303
|
-
// backend has no such provider" — never "unknown". A client id on its own
|
|
304
|
-
// shows nothing, which is silent enough that `warnAboutHalfConfiguredOAuth`
|
|
305
|
-
// below says so out loud.
|
|
306
|
-
const backendProviders = caps.enabledProviders ?? [];
|
|
307
|
-
const hasGoogleLogin = Boolean(googleClientId) && backendProviders.includes("google");
|
|
308
|
-
const hasGitHubLogin = githubClientId && backendProviders.includes("github");
|
|
309
|
-
const hasLinkedinLogin = linkedinClientId && backendProviders.includes("linkedin");
|
|
310
|
-
|
|
311
|
-
useEffect(() => {
|
|
312
|
-
// `enabledProviders` is empty until /auth/config lands, which looks
|
|
313
|
-
// exactly like "the backend has no providers" — so wait for the value
|
|
314
|
-
// to settle. A config that arrives late re-runs this effect and cancels
|
|
315
|
-
// the pending warning.
|
|
316
|
-
const timer = setTimeout(() => {
|
|
317
|
-
warnAboutHalfConfiguredOAuth("google", googleClientId, backendProviders);
|
|
318
|
-
warnAboutHalfConfiguredOAuth("github", githubClientId, backendProviders);
|
|
319
|
-
warnAboutHalfConfiguredOAuth("linkedin", linkedinClientId, backendProviders);
|
|
320
|
-
}, 3000);
|
|
321
|
-
return () => clearTimeout(timer);
|
|
322
|
-
}, [googleClientId, githubClientId, linkedinClientId, backendProviders.join(",")]);
|
|
323
|
-
const hasPasswordReset = caps.passwordReset ?? !!authController.forgotPassword;
|
|
324
|
-
|
|
325
|
-
const showRegistration = !disableSignupScreen && canRegister;
|
|
326
|
-
|
|
327
|
-
useEffect(() => {
|
|
328
|
-
const timer = setTimeout(() => setFadeIn(true), 50);
|
|
329
|
-
return () => clearTimeout(timer);
|
|
330
|
-
}, []);
|
|
331
|
-
|
|
332
|
-
// Effect to handle incoming redirect OAuth codes (GitHub, LinkedIn, etc.).
|
|
333
|
-
//
|
|
334
|
-
// The `code` is only spent when it belongs to an authorization this browser
|
|
335
|
-
// started: `consumeOAuthCallback` matches the returned `state` against the
|
|
336
|
-
// one generated at button-click time. Without that, an attacker who starts
|
|
337
|
-
// their own authorization, captures the code and gets the victim to load
|
|
338
|
-
// `?code=<theirs>` hands the victim a session on the *attacker's* account.
|
|
339
|
-
useEffect(() => {
|
|
340
|
-
const result = consumeOAuthCallback(window.location.search);
|
|
341
|
-
if (result.status === "none") return;
|
|
342
|
-
|
|
343
|
-
// Clear URL search params without page reload
|
|
344
|
-
const cleanUrl = window.location.origin + window.location.pathname;
|
|
345
|
-
window.history.replaceState({}, document.title, cleanUrl);
|
|
346
|
-
|
|
347
|
-
if (result.status === "error") {
|
|
348
|
-
console.error(`OAuth sign-in failed: ${result.error}`);
|
|
349
|
-
if (!isUserCancellation(result.error)) {
|
|
350
|
-
setProviderError(`Sign-in failed: ${result.error}`);
|
|
351
|
-
}
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
if (result.status === "mismatch") {
|
|
355
|
-
console.error("Ignoring an OAuth code that does not match a sign-in this browser started.");
|
|
356
|
-
setProviderError("This sign-in could not be verified as one you started in this browser. Please try again.");
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (authController.oauthLogin) {
|
|
361
|
-
// The rejection is recorded in `authProviderError`, which the screen
|
|
362
|
-
// renders — the catch is here so a shown error does not also surface
|
|
363
|
-
// as an unhandled rejection.
|
|
364
|
-
authController.oauthLogin(result.provider, {
|
|
365
|
-
code: result.code,
|
|
366
|
-
redirectUri: result.redirectUri,
|
|
367
|
-
...(result.codeVerifier ? { codeVerifier: result.codeVerifier } : {})
|
|
368
|
-
}).catch((err: unknown) => {
|
|
369
|
-
console.error(`${result.provider} login failed:`, err);
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
}, [authController]);
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
let logoComponent;
|
|
377
|
-
if (logo) {
|
|
378
|
-
logoComponent = <img src={logo}
|
|
379
|
-
style={{
|
|
380
|
-
height: "100%",
|
|
381
|
-
width: "100%",
|
|
382
|
-
objectFit: "cover"
|
|
383
|
-
}}
|
|
384
|
-
alt={"Logo"}/>;
|
|
385
|
-
} else {
|
|
386
|
-
logoComponent = <RebaseLogo/>;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// The controller's own record of the last rejected sign-in, ignored once a
|
|
390
|
-
// user is present — a stale failure must not sit over a screen that has
|
|
391
|
-
// since succeeded. `LoginForm` renders the same value for the email path.
|
|
392
|
-
const controllerError = authController.authProviderError && !authController.user
|
|
393
|
-
? (authController.authProviderError instanceof Error
|
|
394
|
-
? authController.authProviderError.message
|
|
395
|
-
: String(authController.authProviderError))
|
|
396
|
-
: null;
|
|
397
|
-
const buttonsErrorMessage = providerError ?? controllerError;
|
|
398
|
-
|
|
399
|
-
let notAllowedMessage: string | undefined;
|
|
400
|
-
if (notAllowedError) {
|
|
401
|
-
if (typeof notAllowedError === "string") {
|
|
402
|
-
notAllowedMessage = notAllowedError;
|
|
403
|
-
} else if (notAllowedError instanceof Error) {
|
|
404
|
-
notAllowedMessage = notAllowedError.message;
|
|
405
|
-
} else {
|
|
406
|
-
notAllowedMessage = "It looks like you don't have access, based on the specified access configuration";
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
return (
|
|
411
|
-
<div
|
|
412
|
-
className={cls(
|
|
413
|
-
"relative flex items-center justify-center h-screen w-screen p-4 transition-opacity duration-500 bg-surface-50 dark:bg-surface-900 overflow-hidden",
|
|
414
|
-
fadeIn ? "opacity-100" : "opacity-0"
|
|
415
|
-
)}>
|
|
416
|
-
|
|
417
|
-
{/* Top-right controls */}
|
|
418
|
-
<div className="absolute top-4 right-4 flex items-center gap-1 z-10">
|
|
419
|
-
<LanguageToggle/>
|
|
420
|
-
<Menu
|
|
421
|
-
trigger={<IconButton
|
|
422
|
-
color="inherit"
|
|
423
|
-
aria-label="Toggle theme">
|
|
424
|
-
{colorMode === "dark"
|
|
425
|
-
? <MoonIcon size={iconSize.small}/>
|
|
426
|
-
: <SunIcon size={iconSize.small}/>}
|
|
427
|
-
</IconButton>}>
|
|
428
|
-
<MenuItem onClick={() => setColorMode("dark")}><MoonIcon size={iconSize.smallest}/> {t("dark_mode")}</MenuItem>
|
|
429
|
-
<MenuItem onClick={() => setColorMode("light")}><SunIcon size={iconSize.smallest}/> {t("light_mode")}</MenuItem>
|
|
430
|
-
<MenuItem onClick={() => setColorMode("system")}><SunMoonIcon size={iconSize.smallest}/> {t("system_mode")}</MenuItem>
|
|
431
|
-
</Menu>
|
|
432
|
-
</div>
|
|
433
|
-
|
|
434
|
-
<div className="relative flex flex-col items-center w-[440px] max-w-full p-8 sm:p-10 bg-white/70 dark:bg-surface-800/70 backdrop-blur-xl border border-surface-200/50 dark:border-surface-700/50 rounded-2xl shadow-2xl z-10 transition-all duration-300 hover:shadow-primary-500/5">
|
|
435
|
-
{/* Logo */}
|
|
436
|
-
<div className="w-24 h-24 m-2 mb-4 drop-shadow-md">
|
|
437
|
-
{logoComponent}
|
|
438
|
-
</div>
|
|
439
|
-
|
|
440
|
-
{notAllowedMessage && (
|
|
441
|
-
<div className="p-4 w-full">
|
|
442
|
-
<ErrorView error={notAllowedMessage}/>
|
|
443
|
-
</div>
|
|
444
|
-
)}
|
|
445
|
-
|
|
446
|
-
<div className={cls(
|
|
447
|
-
"w-full transition-opacity duration-150",
|
|
448
|
-
viewVisible ? "opacity-100" : "opacity-0"
|
|
449
|
-
)}>
|
|
450
|
-
|
|
451
|
-
{/* Bootstrap mode: show setup form directly */}
|
|
452
|
-
{isBootstrapMode && !authController.user && (
|
|
453
|
-
<LoginForm
|
|
454
|
-
authController={authController}
|
|
455
|
-
registrationMode={true}
|
|
456
|
-
onClose={() => {}}
|
|
457
|
-
onForgotPassword={() => {}}
|
|
458
|
-
disableSignupScreen={false}
|
|
459
|
-
bootstrapMode={true}
|
|
460
|
-
defaultEmail={defaultEmail}
|
|
461
|
-
defaultPassword={defaultPassword}
|
|
462
|
-
onSignedIn={subscribeIfOptedIn}
|
|
463
|
-
newsletterOptIn={newsletterOptIn}
|
|
464
|
-
setNewsletterOptIn={setNewsletterOptIn}
|
|
465
|
-
showNewsletterOptIn={Boolean(onNewsletterOptIn)}
|
|
466
|
-
/>
|
|
467
|
-
)}
|
|
468
|
-
|
|
469
|
-
{/* Normal mode */}
|
|
470
|
-
{!isBootstrapMode && (
|
|
471
|
-
<>
|
|
472
|
-
{/* Provider buttons screen */}
|
|
473
|
-
{mode === "buttons" && (
|
|
474
|
-
<div className="w-full flex flex-col gap-3 mt-2">
|
|
475
|
-
{/* Every provider button fails here, and this
|
|
476
|
-
screen used to render nothing for it: a
|
|
477
|
-
refused Google sign-in closed the popup
|
|
478
|
-
and returned the visitor to an unchanged
|
|
479
|
-
login screen, with the reason only in the
|
|
480
|
-
browser console. */}
|
|
481
|
-
{buttonsErrorMessage && (
|
|
482
|
-
<ErrorView error={buttonsErrorMessage}/>
|
|
483
|
-
)}
|
|
484
|
-
{(title || subtitle) && (
|
|
485
|
-
<div className="text-center mb-2">
|
|
486
|
-
{title && (
|
|
487
|
-
<Typography variant="h6" className="mb-0.5 font-semibold">
|
|
488
|
-
{title}
|
|
489
|
-
</Typography>
|
|
490
|
-
)}
|
|
491
|
-
{subtitle && (
|
|
492
|
-
<Typography variant="body2" color="secondary" className="mb-4">
|
|
493
|
-
{subtitle}
|
|
494
|
-
</Typography>
|
|
495
|
-
)}
|
|
496
|
-
</div>
|
|
497
|
-
)}
|
|
498
|
-
{topComponent && (
|
|
499
|
-
<div className="w-full">
|
|
500
|
-
{topComponent}
|
|
501
|
-
</div>
|
|
502
|
-
)}
|
|
503
|
-
{/* Above the buttons, not below them: it is a
|
|
504
|
-
condition on the sign-in you are about to
|
|
505
|
-
make, and it reads as one more line of the
|
|
506
|
-
consent block a host puts in
|
|
507
|
-
`topComponent` — the demo's privacy tick
|
|
508
|
-
sits directly above it.
|
|
509
|
-
|
|
510
|
-
Which is why it pulls back most of the
|
|
511
|
-
column's `gap-3`: at the full gap the two
|
|
512
|
-
ticks sat 24px apart and read as two
|
|
513
|
-
unrelated questions rather than one
|
|
514
|
-
consent block. The offset is against this
|
|
515
|
-
column's own gap, not against whatever
|
|
516
|
-
margins the host's `topComponent` happens
|
|
517
|
-
to carry, so it lands the same way under
|
|
518
|
-
any of them — but only when there is
|
|
519
|
-
something above to hug. With no header
|
|
520
|
-
and no `topComponent` this row is the
|
|
521
|
-
column's first child, and pulling back a
|
|
522
|
-
gap that is not there would crowd it into
|
|
523
|
-
the logo. */}
|
|
524
|
-
{onNewsletterOptIn && (
|
|
525
|
-
<label className={cls(
|
|
526
|
-
"flex items-center gap-2 cursor-pointer",
|
|
527
|
-
(topComponent || title || subtitle) && "-mt-3"
|
|
528
|
-
)}>
|
|
529
|
-
<Checkbox
|
|
530
|
-
checked={newsletterOptIn}
|
|
531
|
-
onCheckedChange={(checked) => setNewsletterOptIn(checked === true)}
|
|
532
|
-
size="small"
|
|
533
|
-
/>
|
|
534
|
-
<Typography variant="caption" color="secondary" className="select-none">
|
|
535
|
-
{t("join_newsletter")}
|
|
536
|
-
</Typography>
|
|
537
|
-
</label>
|
|
538
|
-
)}
|
|
539
|
-
<LoginButton
|
|
540
|
-
disabled={disabled}
|
|
541
|
-
text={"Sign in with email"}
|
|
542
|
-
icon={<MailIcon/>}
|
|
543
|
-
onClick={() => switchMode("login")}
|
|
544
|
-
/>
|
|
545
|
-
{hasGoogleLogin && googleClientId && (
|
|
546
|
-
<GoogleLoginButton
|
|
547
|
-
disabled={disabled}
|
|
548
|
-
googleClientId={googleClientId}
|
|
549
|
-
authController={authController}
|
|
550
|
-
onSignedIn={() => subscribeIfOptedIn(authControllerRef.current.user?.email)}
|
|
551
|
-
onError={setProviderError}
|
|
552
|
-
/>
|
|
553
|
-
)}
|
|
554
|
-
{hasGitHubLogin && githubClientId && (
|
|
555
|
-
<GitHubLoginButton
|
|
556
|
-
disabled={disabled}
|
|
557
|
-
githubClientId={githubClientId}
|
|
558
|
-
/>
|
|
559
|
-
)}
|
|
560
|
-
{hasLinkedinLogin && linkedinClientId && (
|
|
561
|
-
<LinkedInLoginButton
|
|
562
|
-
disabled={disabled}
|
|
563
|
-
linkedinClientId={linkedinClientId}
|
|
564
|
-
/>
|
|
565
|
-
)}
|
|
566
|
-
{showRegistration && (
|
|
567
|
-
<div className="mt-2 text-center">
|
|
568
|
-
<Typography variant="body2" color="secondary">
|
|
569
|
-
Don't have an account?{" "}
|
|
570
|
-
<button
|
|
571
|
-
type="button"
|
|
572
|
-
className="font-semibold hover:underline cursor-pointer text-primary-600 dark:text-primary-400"
|
|
573
|
-
onClick={() => switchMode("register")}
|
|
574
|
-
>
|
|
575
|
-
Create one
|
|
576
|
-
</button>
|
|
577
|
-
</Typography>
|
|
578
|
-
</div>
|
|
579
|
-
)}
|
|
580
|
-
</div>
|
|
581
|
-
)}
|
|
582
|
-
|
|
583
|
-
{/* Login form */}
|
|
584
|
-
{mode === "login" && (
|
|
585
|
-
<LoginForm
|
|
586
|
-
authController={authController}
|
|
587
|
-
registrationMode={false}
|
|
588
|
-
onClose={() => switchMode("buttons")}
|
|
589
|
-
onForgotPassword={hasPasswordReset ? () => switchMode("forgot") : undefined}
|
|
590
|
-
noUserComponent={noUserComponent}
|
|
591
|
-
disableSignupScreen={disableSignupScreen}
|
|
592
|
-
switchToRegister={showRegistration ? () => switchMode("register") : undefined}
|
|
593
|
-
defaultEmail={defaultEmail}
|
|
594
|
-
defaultPassword={defaultPassword}
|
|
595
|
-
onSignedIn={subscribeIfOptedIn}
|
|
596
|
-
/>
|
|
597
|
-
)}
|
|
598
|
-
|
|
599
|
-
{/* Registration form */}
|
|
600
|
-
{mode === "register" && (
|
|
601
|
-
<LoginForm
|
|
602
|
-
authController={authController}
|
|
603
|
-
registrationMode={true}
|
|
604
|
-
onClose={() => switchMode("buttons")}
|
|
605
|
-
onForgotPassword={hasPasswordReset ? () => switchMode("forgot") : undefined}
|
|
606
|
-
noUserComponent={noUserComponent}
|
|
607
|
-
disableSignupScreen={disableSignupScreen}
|
|
608
|
-
switchToLogin={() => switchMode("login")}
|
|
609
|
-
defaultEmail={defaultEmail}
|
|
610
|
-
defaultPassword={defaultPassword}
|
|
611
|
-
onSignedIn={subscribeIfOptedIn}
|
|
612
|
-
/>
|
|
613
|
-
)}
|
|
614
|
-
|
|
615
|
-
{/* Forgot password form */}
|
|
616
|
-
{mode === "forgot" && authController.forgotPassword && (
|
|
617
|
-
<ForgotPasswordForm
|
|
618
|
-
authController={authController}
|
|
619
|
-
onClose={() => switchMode("login")}
|
|
620
|
-
/>
|
|
621
|
-
)}
|
|
622
|
-
</>
|
|
623
|
-
)}
|
|
624
|
-
</div>
|
|
625
|
-
|
|
626
|
-
{additionalComponent && !isBootstrapMode && mode === "buttons" && (
|
|
627
|
-
<div className="w-full">
|
|
628
|
-
{additionalComponent}
|
|
629
|
-
</div>
|
|
630
|
-
)}
|
|
631
|
-
</div>
|
|
632
|
-
</div>
|
|
633
|
-
);
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
function LoginButton({
|
|
637
|
-
icon,
|
|
638
|
-
onClick,
|
|
639
|
-
text,
|
|
640
|
-
disabled
|
|
641
|
-
}: { icon: React.ReactNode, onClick: () => void, text: string, disabled?: boolean }) {
|
|
642
|
-
return (
|
|
643
|
-
<Button
|
|
644
|
-
disabled={disabled}
|
|
645
|
-
className="w-full transition-transform duration-200 active:scale-[0.98]"
|
|
646
|
-
variant="outlined"
|
|
647
|
-
size="large"
|
|
648
|
-
onClick={onClick}>
|
|
649
|
-
<div className="flex items-center justify-center w-full gap-3 py-1">
|
|
650
|
-
<span className="flex items-center justify-center w-5 h-5">
|
|
651
|
-
{icon}
|
|
652
|
-
</span>
|
|
653
|
-
<Typography variant="button">{text}</Typography>
|
|
654
|
-
</div>
|
|
655
|
-
</Button>
|
|
656
|
-
);
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
const GoogleIcon = () => (
|
|
660
|
-
<svg viewBox="0 0 24 24" width="20" height="20">
|
|
661
|
-
<path fill="#4285F4"
|
|
662
|
-
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
|
663
|
-
<path fill="#34A853"
|
|
664
|
-
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
|
665
|
-
<path fill="#FBBC05"
|
|
666
|
-
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
|
667
|
-
<path fill="#EA4335"
|
|
668
|
-
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
|
669
|
-
</svg>
|
|
670
|
-
);
|
|
671
|
-
|
|
672
|
-
function GoogleLoginButton({
|
|
673
|
-
disabled,
|
|
674
|
-
googleClientId,
|
|
675
|
-
authController,
|
|
676
|
-
onSignedIn,
|
|
677
|
-
onError
|
|
678
|
-
}: {
|
|
679
|
-
disabled?: boolean,
|
|
680
|
-
googleClientId: string,
|
|
681
|
-
authController: AuthControllerExtended,
|
|
682
|
-
/** Called once the controller has accepted the provider's code. */
|
|
683
|
-
onSignedIn?: () => void,
|
|
684
|
-
/**
|
|
685
|
-
* Reports a failure the auth controller never saw — Google answering the
|
|
686
|
-
* popup with an error, or its script never having loaded. A rejected
|
|
687
|
-
* `googleLogin` is not reported here: the controller already records it.
|
|
688
|
-
*/
|
|
689
|
-
onError?: (message: string | null) => void
|
|
690
|
-
}) {
|
|
691
|
-
const codeClientRef = useRef<{ requestCode(): void } | null>(null);
|
|
692
|
-
// The code client is built once and its callback closes over this render's
|
|
693
|
-
// props forever. A ref keeps the callback reaching the *current* handler
|
|
694
|
-
// rather than the one that existed when Google's script happened to load.
|
|
695
|
-
const onSignedInRef = useRef(onSignedIn);
|
|
696
|
-
onSignedInRef.current = onSignedIn;
|
|
697
|
-
const onErrorRef = useRef(onError);
|
|
698
|
-
onErrorRef.current = onError;
|
|
699
|
-
|
|
700
|
-
useEffect(() => {
|
|
701
|
-
if (!authController.googleLogin) return;
|
|
702
|
-
|
|
703
|
-
const google = window.google;
|
|
704
|
-
if (!google || codeClientRef.current) return;
|
|
705
|
-
|
|
706
|
-
codeClientRef.current = google.accounts.oauth2.initCodeClient({
|
|
707
|
-
client_id: googleClientId,
|
|
708
|
-
scope: "openid email profile",
|
|
709
|
-
ux_mode: "popup",
|
|
710
|
-
callback: async (response: { code?: string; error?: string }) => {
|
|
711
|
-
if (response.error || !response.code) {
|
|
712
|
-
console.error("Google login error:", response.error);
|
|
713
|
-
if (!isUserCancellation(response.error)) {
|
|
714
|
-
onErrorRef.current?.(
|
|
715
|
-
response.error
|
|
716
|
-
? `Google sign-in failed: ${response.error}`
|
|
717
|
-
: "Google sign-in failed. Please try again."
|
|
718
|
-
);
|
|
719
|
-
}
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
722
|
-
onErrorRef.current?.(null);
|
|
723
|
-
try {
|
|
724
|
-
// Send the authorization code to the backend.
|
|
725
|
-
// redirectUri "postmessage" is required when using popup ux_mode.
|
|
726
|
-
await authController.googleLogin!({
|
|
727
|
-
code: response.code,
|
|
728
|
-
redirectUri: "postmessage"
|
|
729
|
-
});
|
|
730
|
-
// After the await, and only on the resolution path: a
|
|
731
|
-
// newsletter tick must not subscribe an address whose
|
|
732
|
-
// sign-in the controller rejected.
|
|
733
|
-
onSignedInRef.current?.();
|
|
734
|
-
} catch (err: unknown) {
|
|
735
|
-
console.error("Google login error:", err);
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
});
|
|
739
|
-
}, [googleClientId, authController]);
|
|
740
|
-
|
|
741
|
-
const handleClick = () => {
|
|
742
|
-
if (!codeClientRef.current) {
|
|
743
|
-
console.error("Google Sign-In not loaded");
|
|
744
|
-
onErrorRef.current?.("Google Sign-In could not be loaded. Check your connection or any script blockers, then try again.");
|
|
745
|
-
return;
|
|
746
|
-
}
|
|
747
|
-
onErrorRef.current?.(null);
|
|
748
|
-
codeClientRef.current.requestCode();
|
|
749
|
-
};
|
|
750
|
-
|
|
751
|
-
return (
|
|
752
|
-
<LoginButton
|
|
753
|
-
disabled={disabled}
|
|
754
|
-
text="Sign in with Google"
|
|
755
|
-
icon={<GoogleIcon/>}
|
|
756
|
-
onClick={handleClick}
|
|
757
|
-
/>
|
|
758
|
-
);
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
const GitHubIcon = () => (
|
|
762
|
-
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
|
763
|
-
<path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.603-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.464-1.11-1.464-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.579.688.481C19.137 20.162 22 16.418 22 12c0-5.523-4.477-10-10-10z"/>
|
|
764
|
-
</svg>
|
|
765
|
-
);
|
|
766
|
-
|
|
767
|
-
function GitHubLoginButton({
|
|
768
|
-
disabled,
|
|
769
|
-
githubClientId
|
|
770
|
-
}: {
|
|
771
|
-
disabled?: boolean,
|
|
772
|
-
githubClientId: string
|
|
773
|
-
}) {
|
|
774
|
-
const handleClick = () => {
|
|
775
|
-
// GitHub OAuth Apps do not implement PKCE, so `state` is the whole
|
|
776
|
-
// binding between this click and the code that comes back.
|
|
777
|
-
void startOAuthRedirect({
|
|
778
|
-
provider: "github",
|
|
779
|
-
authorizeUrl: "https://github.com/login/oauth/authorize",
|
|
780
|
-
clientId: githubClientId,
|
|
781
|
-
redirectUri: window.location.origin + window.location.pathname,
|
|
782
|
-
scope: "read:user,user:email"
|
|
783
|
-
});
|
|
784
|
-
};
|
|
785
|
-
|
|
786
|
-
return (
|
|
787
|
-
<LoginButton
|
|
788
|
-
disabled={disabled}
|
|
789
|
-
text="Sign in with GitHub"
|
|
790
|
-
icon={<GitHubIcon/>}
|
|
791
|
-
onClick={handleClick}
|
|
792
|
-
/>
|
|
793
|
-
);
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
const LinkedInIcon = () => (
|
|
797
|
-
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
|
798
|
-
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/>
|
|
799
|
-
</svg>
|
|
800
|
-
);
|
|
801
|
-
|
|
802
|
-
function LinkedInLoginButton({
|
|
803
|
-
disabled,
|
|
804
|
-
linkedinClientId
|
|
805
|
-
}: {
|
|
806
|
-
disabled?: boolean,
|
|
807
|
-
linkedinClientId: string
|
|
808
|
-
}) {
|
|
809
|
-
const handleClick = () => {
|
|
810
|
-
void startOAuthRedirect({
|
|
811
|
-
provider: "linkedin",
|
|
812
|
-
authorizeUrl: "https://www.linkedin.com/oauth/v2/authorization",
|
|
813
|
-
clientId: linkedinClientId,
|
|
814
|
-
redirectUri: window.location.origin + window.location.pathname,
|
|
815
|
-
scope: "openid profile email"
|
|
816
|
-
});
|
|
817
|
-
};
|
|
818
|
-
|
|
819
|
-
return (
|
|
820
|
-
<LoginButton
|
|
821
|
-
disabled={disabled}
|
|
822
|
-
text="Sign in with LinkedIn"
|
|
823
|
-
icon={<LinkedInIcon/>}
|
|
824
|
-
onClick={handleClick}
|
|
825
|
-
/>
|
|
826
|
-
);
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
function LoginForm({
|
|
830
|
-
onClose,
|
|
831
|
-
onForgotPassword,
|
|
832
|
-
authController,
|
|
833
|
-
registrationMode,
|
|
834
|
-
noUserComponent,
|
|
835
|
-
disableSignupScreen,
|
|
836
|
-
bootstrapMode = false,
|
|
837
|
-
switchToRegister,
|
|
838
|
-
switchToLogin,
|
|
839
|
-
defaultEmail,
|
|
840
|
-
defaultPassword,
|
|
841
|
-
onSignedIn,
|
|
842
|
-
newsletterOptIn = false,
|
|
843
|
-
setNewsletterOptIn,
|
|
844
|
-
showNewsletterOptIn = false
|
|
845
|
-
}: {
|
|
846
|
-
onClose: () => void,
|
|
847
|
-
onForgotPassword?: () => void,
|
|
848
|
-
authController: AuthControllerExtended,
|
|
849
|
-
registrationMode: boolean,
|
|
850
|
-
noUserComponent?: ReactNode,
|
|
851
|
-
disableSignupScreen: boolean,
|
|
852
|
-
bootstrapMode?: boolean,
|
|
853
|
-
switchToRegister?: () => void,
|
|
854
|
-
switchToLogin?: () => void,
|
|
855
|
-
defaultEmail?: string,
|
|
856
|
-
defaultPassword?: string,
|
|
857
|
-
/** The address the controller just accepted, reported after every success. */
|
|
858
|
-
onSignedIn?: (email: string) => void,
|
|
859
|
-
newsletterOptIn?: boolean,
|
|
860
|
-
setNewsletterOptIn?: (checked: boolean) => void,
|
|
861
|
-
/**
|
|
862
|
-
* Render the newsletter opt-in here. Only bootstrap mode does: everywhere
|
|
863
|
-
* else it lives on the provider screen, which bootstrap mode never shows.
|
|
864
|
-
*/
|
|
865
|
-
showNewsletterOptIn?: boolean
|
|
866
|
-
}) {
|
|
867
|
-
const passwordRef = useRef<HTMLInputElement | null>(null);
|
|
868
|
-
const { t } = useTranslation();
|
|
869
|
-
|
|
870
|
-
// `Typography variant="label"` already renders a real <label> element, it just
|
|
871
|
-
// never pointed at anything, so each field fell back to naming itself after its
|
|
872
|
-
// own placeholder — the password field announced as eight bullets. Associating
|
|
873
|
-
// the existing labels beats passing the kit's `label` prop here: that one moves
|
|
874
|
-
// the text inside the box (`pt-6 pb-2` at this size) and hides the placeholder
|
|
875
|
-
// until focus, which would redesign the login screen to fix a name.
|
|
876
|
-
const emailId = useId();
|
|
877
|
-
const passwordId = useId();
|
|
878
|
-
const displayNameId = useId();
|
|
879
|
-
|
|
880
|
-
const [email, setEmail] = useState<string | undefined>(defaultEmail);
|
|
881
|
-
const [password, setPassword] = useState<string | undefined>(defaultPassword);
|
|
882
|
-
const [displayName, setDisplayName] = useState<string>();
|
|
883
|
-
|
|
884
|
-
useEffect(() => {
|
|
885
|
-
if (!document) return;
|
|
886
|
-
const escFunction = (event: KeyboardEvent) => {
|
|
887
|
-
if (event.key === "Escape") {
|
|
888
|
-
onClose();
|
|
889
|
-
}
|
|
890
|
-
};
|
|
891
|
-
document.addEventListener("keydown", escFunction, false);
|
|
892
|
-
return () => {
|
|
893
|
-
document.removeEventListener("keydown", escFunction, false);
|
|
894
|
-
};
|
|
895
|
-
}, [onClose]);
|
|
896
|
-
|
|
897
|
-
// Both controller methods record the failure in `authProviderError` — which
|
|
898
|
-
// this form renders above — and then rethrow for programmatic callers. This
|
|
899
|
-
// is not one of those: without a catch here every rejected sign-in becomes an
|
|
900
|
-
// unhandled rejection, reported as a crash by anything listening for one,
|
|
901
|
-
// while the user has already been shown the error.
|
|
902
|
-
// Fires only on the resolution path, i.e. after the controller accepted
|
|
903
|
-
// the credentials — a ticked box on a failed attempt must not subscribe
|
|
904
|
-
// an address its owner never proved they control.
|
|
905
|
-
function reportSignedIn(email: string) {
|
|
906
|
-
onSignedIn?.(email);
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
function handleEnterPassword() {
|
|
910
|
-
if (email && password && authController.emailPasswordLogin) {
|
|
911
|
-
void Promise.resolve(authController.emailPasswordLogin(email, password))
|
|
912
|
-
.then(() => reportSignedIn(email))
|
|
913
|
-
.catch(() => undefined);
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
function handleRegistration() {
|
|
918
|
-
if (email && password && authController.register) {
|
|
919
|
-
void Promise.resolve(authController.register(email, password, displayName))
|
|
920
|
-
.then(() => reportSignedIn(email))
|
|
921
|
-
.catch(() => undefined);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
const handleSubmit = (event: React.FormEvent) => {
|
|
926
|
-
event.preventDefault();
|
|
927
|
-
if (registrationMode)
|
|
928
|
-
handleRegistration();
|
|
929
|
-
else
|
|
930
|
-
handleEnterPassword();
|
|
931
|
-
};
|
|
932
|
-
|
|
933
|
-
const title = bootstrapMode
|
|
934
|
-
? "Welcome!"
|
|
935
|
-
: registrationMode
|
|
936
|
-
? "Create account"
|
|
937
|
-
: "Sign in";
|
|
938
|
-
|
|
939
|
-
const subtitle = bootstrapMode
|
|
940
|
-
? "Create your admin account to get started. This account will have admin privileges."
|
|
941
|
-
: registrationMode
|
|
942
|
-
? "Fill in your details to create a new account"
|
|
943
|
-
: "Enter your credentials to continue";
|
|
944
|
-
|
|
945
|
-
const buttonLabel = registrationMode ? "Create account" : "Sign in";
|
|
946
|
-
|
|
947
|
-
return (
|
|
948
|
-
<form onSubmit={handleSubmit} className="flex flex-col w-full gap-1 mt-2">
|
|
949
|
-
{!bootstrapMode && (
|
|
950
|
-
<div className="w-full mb-2 -ml-2.5">
|
|
951
|
-
<IconButton onClick={onClose}>
|
|
952
|
-
<ArrowLeftIcon/>
|
|
953
|
-
</IconButton>
|
|
954
|
-
</div>
|
|
955
|
-
)}
|
|
956
|
-
|
|
957
|
-
{(() => {
|
|
958
|
-
const err = authController.authProviderError;
|
|
959
|
-
if (!err || authController.user) return null;
|
|
960
|
-
const msg: string = err instanceof Error ? err.message : String(err);
|
|
961
|
-
return (
|
|
962
|
-
<div className="w-full mb-2">
|
|
963
|
-
<ErrorView error={msg}/>
|
|
964
|
-
</div>
|
|
965
|
-
);
|
|
966
|
-
})()}
|
|
967
|
-
|
|
968
|
-
<Typography variant="h6" className="mb-0.5">
|
|
969
|
-
{title}
|
|
970
|
-
</Typography>
|
|
971
|
-
<Typography variant="body2" color="secondary" className="mb-5">
|
|
972
|
-
{subtitle}
|
|
973
|
-
</Typography>
|
|
974
|
-
|
|
975
|
-
{registrationMode && !bootstrapMode && noUserComponent && (
|
|
976
|
-
<div className="w-full mb-2">
|
|
977
|
-
{noUserComponent}
|
|
978
|
-
</div>
|
|
979
|
-
)}
|
|
980
|
-
|
|
981
|
-
{registrationMode && (
|
|
982
|
-
<div className="w-full mb-3">
|
|
983
|
-
<Typography variant="label" component="label" color="secondary" className="mb-1" htmlFor={displayNameId}>
|
|
984
|
-
Display Name
|
|
985
|
-
</Typography>
|
|
986
|
-
<TextField placeholder="Jane Doe (optional)"
|
|
987
|
-
id={displayNameId}
|
|
988
|
-
className={loginFieldClasses}
|
|
989
|
-
value={displayName ?? ""}
|
|
990
|
-
disabled={authController.initialLoading}
|
|
991
|
-
type="text"
|
|
992
|
-
size="medium"
|
|
993
|
-
onChange={(event) => setDisplayName(event.target.value)}/>
|
|
994
|
-
</div>
|
|
995
|
-
)}
|
|
996
|
-
|
|
997
|
-
<div className="w-full mb-3">
|
|
998
|
-
<Typography variant="label" component="label" color="secondary" className="mb-1" htmlFor={emailId}>
|
|
999
|
-
Email
|
|
1000
|
-
</Typography>
|
|
1001
|
-
<TextField placeholder="you@example.com"
|
|
1002
|
-
id={emailId}
|
|
1003
|
-
className={loginFieldClasses}
|
|
1004
|
-
autoFocus
|
|
1005
|
-
value={email ?? ""}
|
|
1006
|
-
disabled={authController.initialLoading}
|
|
1007
|
-
type="email"
|
|
1008
|
-
size="medium"
|
|
1009
|
-
onChange={(event) => setEmail(event.target.value)}/>
|
|
1010
|
-
</div>
|
|
1011
|
-
|
|
1012
|
-
<div className="w-full mb-1">
|
|
1013
|
-
<Typography variant="label" component="label" color="secondary" className="mb-1" htmlFor={passwordId}>
|
|
1014
|
-
Password
|
|
1015
|
-
</Typography>
|
|
1016
|
-
<TextField placeholder="••••••••"
|
|
1017
|
-
id={passwordId}
|
|
1018
|
-
className={loginFieldClasses}
|
|
1019
|
-
value={password ?? ""}
|
|
1020
|
-
disabled={authController.initialLoading}
|
|
1021
|
-
inputRef={passwordRef}
|
|
1022
|
-
type="password"
|
|
1023
|
-
size="medium"
|
|
1024
|
-
onChange={(event) => setPassword(event.target.value)}/>
|
|
1025
|
-
</div>
|
|
1026
|
-
|
|
1027
|
-
{registrationMode && (
|
|
1028
|
-
<Typography variant="caption" color="secondary" className="mb-3">
|
|
1029
|
-
Password must be 8+ characters with uppercase, lowercase, and a number
|
|
1030
|
-
</Typography>
|
|
1031
|
-
)}
|
|
1032
|
-
|
|
1033
|
-
{!registrationMode && onForgotPassword && (
|
|
1034
|
-
<div className="w-full text-right mb-3">
|
|
1035
|
-
<button
|
|
1036
|
-
type="button"
|
|
1037
|
-
className="text-xs font-medium hover:underline cursor-pointer text-primary-600 dark:text-primary-400"
|
|
1038
|
-
onClick={onForgotPassword}
|
|
1039
|
-
>
|
|
1040
|
-
Forgot password?
|
|
1041
|
-
</button>
|
|
1042
|
-
</div>
|
|
1043
|
-
)}
|
|
1044
|
-
|
|
1045
|
-
{/* Bootstrap mode only — see `showNewsletterOptIn`. Every other
|
|
1046
|
-
route to this form came through the provider screen, which is
|
|
1047
|
-
where the box now is. */}
|
|
1048
|
-
{showNewsletterOptIn && (
|
|
1049
|
-
<label className="flex items-center gap-2 cursor-pointer mt-1 mb-1">
|
|
1050
|
-
<Checkbox
|
|
1051
|
-
checked={newsletterOptIn}
|
|
1052
|
-
onCheckedChange={(checked) => setNewsletterOptIn?.(checked === true)}
|
|
1053
|
-
size="small"
|
|
1054
|
-
/>
|
|
1055
|
-
<Typography variant="caption" color="secondary" className="select-none">
|
|
1056
|
-
{t("join_newsletter")}
|
|
1057
|
-
</Typography>
|
|
1058
|
-
</label>
|
|
1059
|
-
)}
|
|
1060
|
-
|
|
1061
|
-
<LoadingButton
|
|
1062
|
-
type="submit"
|
|
1063
|
-
variant="filled"
|
|
1064
|
-
color="primary"
|
|
1065
|
-
className="w-full mt-1"
|
|
1066
|
-
size="large"
|
|
1067
|
-
loading={authController.authLoading}
|
|
1068
|
-
disabled={authController.authLoading || !email || !password}
|
|
1069
|
-
>
|
|
1070
|
-
{buttonLabel}
|
|
1071
|
-
</LoadingButton>
|
|
1072
|
-
|
|
1073
|
-
{/* Switch between login/register */}
|
|
1074
|
-
{switchToRegister && (
|
|
1075
|
-
<div className="mt-4 text-center">
|
|
1076
|
-
<Typography variant="body2" color="secondary">
|
|
1077
|
-
Don't have an account?{" "}
|
|
1078
|
-
<button
|
|
1079
|
-
type="button"
|
|
1080
|
-
className="font-semibold hover:underline cursor-pointer text-primary-600 dark:text-primary-400"
|
|
1081
|
-
onClick={switchToRegister}
|
|
1082
|
-
>
|
|
1083
|
-
Create one
|
|
1084
|
-
</button>
|
|
1085
|
-
</Typography>
|
|
1086
|
-
</div>
|
|
1087
|
-
)}
|
|
1088
|
-
|
|
1089
|
-
{switchToLogin && (
|
|
1090
|
-
<div className="mt-4 text-center">
|
|
1091
|
-
<Typography variant="body2" color="secondary">
|
|
1092
|
-
Already have an account?{" "}
|
|
1093
|
-
<button
|
|
1094
|
-
type="button"
|
|
1095
|
-
className="font-semibold hover:underline cursor-pointer text-primary-600 dark:text-primary-400"
|
|
1096
|
-
onClick={switchToLogin}
|
|
1097
|
-
>
|
|
1098
|
-
Sign in
|
|
1099
|
-
</button>
|
|
1100
|
-
</Typography>
|
|
1101
|
-
</div>
|
|
1102
|
-
)}
|
|
1103
|
-
</form>
|
|
1104
|
-
);
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
function ForgotPasswordForm({
|
|
1108
|
-
onClose,
|
|
1109
|
-
authController
|
|
1110
|
-
}: {
|
|
1111
|
-
onClose: () => void,
|
|
1112
|
-
authController: AuthControllerExtended
|
|
1113
|
-
}) {
|
|
1114
|
-
const [email, setEmail] = useState<string>("");
|
|
1115
|
-
const [submitted, setSubmitted] = useState(false);
|
|
1116
|
-
const [error, setError] = useState<string | null>(null);
|
|
1117
|
-
const emailId = useId();
|
|
1118
|
-
|
|
1119
|
-
useEffect(() => {
|
|
1120
|
-
if (!document) return;
|
|
1121
|
-
const escFunction = (event: KeyboardEvent) => {
|
|
1122
|
-
if (event.key === "Escape") {
|
|
1123
|
-
onClose();
|
|
1124
|
-
}
|
|
1125
|
-
};
|
|
1126
|
-
document.addEventListener("keydown", escFunction, false);
|
|
1127
|
-
return () => {
|
|
1128
|
-
document.removeEventListener("keydown", escFunction, false);
|
|
1129
|
-
};
|
|
1130
|
-
}, [onClose]);
|
|
1131
|
-
|
|
1132
|
-
const handleSubmit = async (event: React.FormEvent) => {
|
|
1133
|
-
event.preventDefault();
|
|
1134
|
-
setError(null);
|
|
1135
|
-
|
|
1136
|
-
if (!email) {
|
|
1137
|
-
setError("Please enter your email address");
|
|
1138
|
-
return;
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
if (!authController.forgotPassword) {
|
|
1142
|
-
setError("Password reset is not available with this auth provider.");
|
|
1143
|
-
return;
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
try {
|
|
1147
|
-
await authController.forgotPassword(email);
|
|
1148
|
-
setSubmitted(true);
|
|
1149
|
-
} catch (err: unknown) {
|
|
1150
|
-
// Check for EMAIL_NOT_CONFIGURED error
|
|
1151
|
-
if (err instanceof Error && (err as { code?: string }).code === "EMAIL_NOT_CONFIGURED") {
|
|
1152
|
-
setError("Password reset is not available. Please contact your administrator.");
|
|
1153
|
-
} else {
|
|
1154
|
-
// Still show success (security: don't reveal if email exists)
|
|
1155
|
-
setSubmitted(true);
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
};
|
|
1159
|
-
|
|
1160
|
-
if (submitted) {
|
|
1161
|
-
return (
|
|
1162
|
-
<div className="flex flex-col w-full gap-4 mt-2">
|
|
1163
|
-
<div className="w-full -ml-2.5">
|
|
1164
|
-
<IconButton onClick={onClose}>
|
|
1165
|
-
<ArrowLeftIcon/>
|
|
1166
|
-
</IconButton>
|
|
1167
|
-
</div>
|
|
1168
|
-
|
|
1169
|
-
<div className="text-center rounded-xl p-6 bg-surface-50 dark:bg-surface-900/60 dark:border dark:border-surface-700/50">
|
|
1170
|
-
<div className="text-3xl mb-3">📧</div>
|
|
1171
|
-
<Typography variant="subtitle1" className="mb-2">
|
|
1172
|
-
Check your email
|
|
1173
|
-
</Typography>
|
|
1174
|
-
<Typography variant="body2" color="secondary">
|
|
1175
|
-
If an account exists for <strong>{email}</strong>, you'll receive a password reset link shortly.
|
|
1176
|
-
</Typography>
|
|
1177
|
-
</div>
|
|
1178
|
-
|
|
1179
|
-
<Button onClick={onClose} variant="text" className="mt-2">
|
|
1180
|
-
Back to sign in
|
|
1181
|
-
</Button>
|
|
1182
|
-
</div>
|
|
1183
|
-
);
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
return (
|
|
1187
|
-
<form onSubmit={handleSubmit} className="flex flex-col w-full gap-1 mt-2">
|
|
1188
|
-
<div className="w-full mb-2 -ml-2.5">
|
|
1189
|
-
<IconButton onClick={onClose}>
|
|
1190
|
-
<ArrowLeftIcon/>
|
|
1191
|
-
</IconButton>
|
|
1192
|
-
</div>
|
|
1193
|
-
|
|
1194
|
-
<Typography variant="h6" className="mb-0.5">
|
|
1195
|
-
Reset password
|
|
1196
|
-
</Typography>
|
|
1197
|
-
<Typography variant="body2" color="secondary" className="mb-5">
|
|
1198
|
-
Enter your email and we'll send you a reset link.
|
|
1199
|
-
</Typography>
|
|
1200
|
-
|
|
1201
|
-
{error && (
|
|
1202
|
-
<div className="w-full mb-3">
|
|
1203
|
-
<ErrorView error={error}/>
|
|
1204
|
-
</div>
|
|
1205
|
-
)}
|
|
1206
|
-
|
|
1207
|
-
<div className="w-full mb-3">
|
|
1208
|
-
<Typography variant="label" component="label" color="secondary" className="mb-1" htmlFor={emailId}>
|
|
1209
|
-
Email
|
|
1210
|
-
</Typography>
|
|
1211
|
-
<TextField
|
|
1212
|
-
placeholder="you@example.com"
|
|
1213
|
-
id={emailId}
|
|
1214
|
-
className={loginFieldClasses}
|
|
1215
|
-
autoFocus
|
|
1216
|
-
value={email}
|
|
1217
|
-
type="email"
|
|
1218
|
-
size="medium"
|
|
1219
|
-
onChange={(event) => setEmail(event.target.value)}
|
|
1220
|
-
/>
|
|
1221
|
-
</div>
|
|
1222
|
-
|
|
1223
|
-
<LoadingButton
|
|
1224
|
-
type="submit"
|
|
1225
|
-
variant="filled"
|
|
1226
|
-
className="w-full"
|
|
1227
|
-
size="large"
|
|
1228
|
-
loading={authController.authLoading}
|
|
1229
|
-
disabled={authController.authLoading || !email}
|
|
1230
|
-
>
|
|
1231
|
-
Send reset link
|
|
1232
|
-
</LoadingButton>
|
|
1233
|
-
</form>
|
|
1234
|
-
);
|
|
1235
|
-
}
|