@rebasepro/app 0.0.1-canary.4829d6e
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/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/auth/api.d.ts +66 -0
- package/dist/auth/index.d.ts +13 -0
- package/dist/auth/types.d.ts +127 -0
- package/dist/auth/useRebaseAuthController.d.ts +13 -0
- package/dist/components/AIIcon.d.ts +17 -0
- package/dist/components/AuthSimulationMenu.d.ts +0 -0
- package/dist/components/ConfirmationDialog.d.ts +9 -0
- package/dist/components/Debug/UIReferenceView.d.ts +13 -0
- package/dist/components/Debug/UIStyleGuide.d.ts +2 -0
- package/dist/components/Debug/collection-views/CardViewDemo.d.ts +2 -0
- package/dist/components/Debug/collection-views/CollectionTableDemo.d.ts +2 -0
- package/dist/components/Debug/collection-views/KanbanBoardDemo.d.ts +2 -0
- package/dist/components/Debug/collection-views/index.d.ts +3 -0
- package/dist/components/Debug/collection-views/sample_data.d.ts +23 -0
- package/dist/components/Debug/crm-dashboard/CalendarWidget.d.ts +22 -0
- package/dist/components/Debug/crm-dashboard/CrmDashboardDemo.d.ts +2 -0
- package/dist/components/Debug/crm-dashboard/DashboardMetrics.d.ts +15 -0
- package/dist/components/Debug/crm-dashboard/PipelineOverview.d.ts +34 -0
- package/dist/components/Debug/crm-dashboard/RecentActivity.d.ts +19 -0
- package/dist/components/Debug/crm-dashboard/TaskFilters.d.ts +30 -0
- package/dist/components/Debug/crm-dashboard/TaskTable.d.ts +50 -0
- package/dist/components/Debug/crm-dashboard/TasksView.d.ts +41 -0
- package/dist/components/ErrorTooltip.d.ts +3 -0
- package/dist/components/ErrorView.d.ts +21 -0
- package/dist/components/LanguageToggle.d.ts +2 -0
- package/dist/components/LoginView/LoginView.d.ts +113 -0
- package/dist/components/LoginView/index.d.ts +2 -0
- package/dist/components/NotFoundPage.d.ts +2 -0
- package/dist/components/RebaseAuth.d.ts +10 -0
- package/dist/components/RebaseLogo.d.ts +7 -0
- package/dist/components/SchemaDriftBanner.d.ts +27 -0
- package/dist/components/UnsavedChangesDialog.d.ts +9 -0
- package/dist/components/UserDisplay.d.ts +7 -0
- package/dist/components/UserSelectPopover.d.ts +63 -0
- package/dist/components/UserSettingsView.d.ts +2 -0
- package/dist/components/common/index.d.ts +6 -0
- package/dist/components/common/table_height.d.ts +5 -0
- package/dist/components/common/types.d.ts +66 -0
- package/dist/components/common/useColumnsIds.d.ts +9 -0
- package/dist/components/common/useDataTableController.d.ts +45 -0
- package/dist/components/common/useDebouncedData.d.ts +9 -0
- package/dist/components/common/useScrollRestoration.d.ts +14 -0
- package/dist/components/index.d.ts +18 -0
- package/dist/contexts/AdminModeController.d.ts +4 -0
- package/dist/contexts/AnalyticsContext.d.ts +3 -0
- package/dist/contexts/AuthControllerContext.d.ts +3 -0
- package/dist/contexts/CollectionScopeContext.d.ts +50 -0
- package/dist/contexts/ComponentOverrideContext.d.ts +42 -0
- package/dist/contexts/CustomizationControllerContext.d.ts +3 -0
- package/dist/contexts/DataDriverContext.d.ts +3 -0
- package/dist/contexts/DataSourcesContext.d.ts +31 -0
- package/dist/contexts/DatabaseAdminContext.d.ts +3 -0
- package/dist/contexts/DialogsProvider.d.ts +4 -0
- package/dist/contexts/EffectiveRoleController.d.ts +4 -0
- package/dist/contexts/ModeController.d.ts +4 -0
- package/dist/contexts/RebaseClientInstanceContext.d.ts +6 -0
- package/dist/contexts/RebaseDataContext.d.ts +3 -0
- package/dist/contexts/SnackbarProvider.d.ts +2 -0
- package/dist/contexts/StorageSourceContext.d.ts +3 -0
- package/dist/contexts/StorageSourcesContext.d.ts +25 -0
- package/dist/contexts/UserConfigurationPersistenceContext.d.ts +3 -0
- package/dist/contexts/index.d.ts +17 -0
- package/dist/core/PluginLifecycleManager.d.ts +17 -0
- package/dist/core/PluginProviderStack.d.ts +21 -0
- package/dist/core/Rebase.d.ts +15 -0
- package/dist/core/RebaseProps.d.ts +291 -0
- package/dist/core/RebaseRouter.d.ts +4 -0
- package/dist/core/RebaseRoutes.d.ts +17 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/hooks/ApiConfigContext.d.ts +24 -0
- package/dist/hooks/data/delete.d.ts +33 -0
- package/dist/hooks/data/save.d.ts +40 -0
- package/dist/hooks/data/useCollection.d.ts +62 -0
- package/dist/hooks/data/useData.d.ts +13 -0
- package/dist/hooks/data/useDataOrder.d.ts +12 -0
- package/dist/hooks/data/useFetch.d.ts +43 -0
- package/dist/hooks/data/useRelationSelector.d.ts +52 -0
- package/dist/hooks/index.d.ts +36 -0
- package/dist/hooks/useAdminModeController.d.ts +19 -0
- package/dist/hooks/useAnalyticsController.d.ts +5 -0
- package/dist/hooks/useAuthController.d.ts +11 -0
- package/dist/hooks/useAuthSubscription.d.ts +2 -0
- package/dist/hooks/useBackendStorageSource.d.ts +30 -0
- package/dist/hooks/useBridgeRegistration.d.ts +18 -0
- package/dist/hooks/useBrowserTitleAndIcon.d.ts +6 -0
- package/dist/hooks/useBuildAdminModeController.d.ts +6 -0
- package/dist/hooks/useBuildEffectiveRoleController.d.ts +8 -0
- package/dist/hooks/useBuildLocalConfigurationPersistence.d.ts +2 -0
- package/dist/hooks/useBuildModeController.d.ts +6 -0
- package/dist/hooks/useClipboard.d.ts +57 -0
- package/dist/hooks/useCollapsedGroups.d.ts +27 -0
- package/dist/hooks/useComponentOverride.d.ts +32 -0
- package/dist/hooks/useCustomizationController.d.ts +11 -0
- package/dist/hooks/useDialogsController.d.ts +11 -0
- package/dist/hooks/useEffectiveRoleController.d.ts +7 -0
- package/dist/hooks/useLargeLayout.d.ts +1 -0
- package/dist/hooks/useModeController.d.ts +19 -0
- package/dist/hooks/usePermissions.d.ts +12 -0
- package/dist/hooks/useRebaseClient.d.ts +5 -0
- package/dist/hooks/useRebaseContext.d.ts +11 -0
- package/dist/hooks/useRebaseRegistry.d.ts +34 -0
- package/dist/hooks/useResolvedComponent.d.ts +47 -0
- package/dist/hooks/useSlot.d.ts +18 -0
- package/dist/hooks/useSnackbarController.d.ts +20 -0
- package/dist/hooks/useStorageSource.d.ts +6 -0
- package/dist/hooks/useStudioBridge.d.ts +79 -0
- package/dist/hooks/useTranslation.d.ts +17 -0
- package/dist/hooks/useUnsavedChangesDialog.d.ts +12 -0
- package/dist/hooks/useUserConfigurationPersistence.d.ts +8 -0
- package/dist/i18n/RebaseI18nProvider.d.ts +33 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.es.js +16843 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +17021 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/internal/common.d.ts +14 -0
- package/dist/internal/useRestoreScroll.d.ts +15 -0
- package/dist/locales/de.d.ts +2 -0
- package/dist/locales/en.d.ts +10 -0
- package/dist/locales/es.d.ts +10 -0
- package/dist/locales/fr.d.ts +2 -0
- package/dist/locales/hi.d.ts +2 -0
- package/dist/locales/it.d.ts +2 -0
- package/dist/locales/pt.d.ts +7 -0
- package/dist/util/constants.d.ts +1 -0
- package/dist/util/createFormexStub.d.ts +2 -0
- package/dist/util/entity_cache.d.ts +22 -0
- package/dist/util/enums.d.ts +5 -0
- package/dist/util/icon_list.d.ts +5 -0
- package/dist/util/icons.d.ts +20 -0
- package/dist/util/index.d.ts +8 -0
- package/dist/util/previews.d.ts +4 -0
- package/dist/util/useStorageUploadController.d.ts +44 -0
- package/dist/vitePlugin.d.ts +60 -0
- package/dist/vitePlugin.js +113 -0
- package/package.json +145 -0
- package/src/auth/api.ts +148 -0
- package/src/auth/index.ts +25 -0
- package/src/auth/types.ts +115 -0
- package/src/auth/useRebaseAuthController.ts +328 -0
- package/src/components/AIIcon.tsx +47 -0
- package/src/components/AuthSimulationMenu.tsx +0 -0
- package/src/components/ConfirmationDialog.tsx +48 -0
- package/src/components/Debug/UIReferenceView.tsx +958 -0
- package/src/components/Debug/UIStyleGuide.tsx +156 -0
- package/src/components/Debug/collection-views/CardViewDemo.tsx +85 -0
- package/src/components/Debug/collection-views/CollectionTableDemo.tsx +107 -0
- package/src/components/Debug/collection-views/KanbanBoardDemo.tsx +90 -0
- package/src/components/Debug/collection-views/index.ts +3 -0
- package/src/components/Debug/collection-views/sample_data.ts +42 -0
- package/src/components/Debug/crm-dashboard/CalendarWidget.tsx +389 -0
- package/src/components/Debug/crm-dashboard/CrmDashboardDemo.tsx +1102 -0
- package/src/components/Debug/crm-dashboard/DashboardMetrics.tsx +133 -0
- package/src/components/Debug/crm-dashboard/PipelineOverview.tsx +290 -0
- package/src/components/Debug/crm-dashboard/RecentActivity.tsx +215 -0
- package/src/components/Debug/crm-dashboard/TaskFilters.tsx +184 -0
- package/src/components/Debug/crm-dashboard/TaskTable.tsx +427 -0
- package/src/components/Debug/crm-dashboard/TasksView.tsx +234 -0
- package/src/components/ErrorTooltip.tsx +12 -0
- package/src/components/ErrorView.tsx +94 -0
- package/src/components/LanguageToggle.tsx +59 -0
- package/src/components/LoginView/LoginView.tsx +932 -0
- package/src/components/LoginView/index.ts +2 -0
- package/src/components/NotFoundPage.tsx +27 -0
- package/src/components/RebaseAuth.tsx +27 -0
- package/src/components/RebaseLogo.tsx +29 -0
- package/src/components/SchemaDriftBanner.tsx +102 -0
- package/src/components/UnsavedChangesDialog.tsx +46 -0
- package/src/components/UserDisplay.tsx +54 -0
- package/src/components/UserSelectPopover.tsx +399 -0
- package/src/components/UserSettingsView.tsx +318 -0
- package/src/components/common/index.ts +6 -0
- package/src/components/common/table_height.tsx +21 -0
- package/src/components/common/types.tsx +66 -0
- package/src/components/common/useColumnsIds.tsx +222 -0
- package/src/components/common/useDataTableController.tsx +514 -0
- package/src/components/common/useDebouncedData.ts +49 -0
- package/src/components/common/useScrollRestoration.tsx +68 -0
- package/src/components/index.tsx +26 -0
- package/src/contexts/AdminModeController.tsx +11 -0
- package/src/contexts/AnalyticsContext.tsx +4 -0
- package/src/contexts/AuthControllerContext.tsx +4 -0
- package/src/contexts/CollectionScopeContext.tsx +67 -0
- package/src/contexts/ComponentOverrideContext.tsx +81 -0
- package/src/contexts/CustomizationControllerContext.tsx +4 -0
- package/src/contexts/DataDriverContext.tsx +4 -0
- package/src/contexts/DataSourcesContext.tsx +36 -0
- package/src/contexts/DatabaseAdminContext.tsx +4 -0
- package/src/contexts/DialogsProvider.tsx +56 -0
- package/src/contexts/EffectiveRoleController.tsx +12 -0
- package/src/contexts/ModeController.tsx +11 -0
- package/src/contexts/RebaseClientInstanceContext.tsx +7 -0
- package/src/contexts/RebaseDataContext.tsx +4 -0
- package/src/contexts/SnackbarProvider.tsx +15 -0
- package/src/contexts/StorageSourceContext.tsx +4 -0
- package/src/contexts/StorageSourcesContext.tsx +30 -0
- package/src/contexts/UserConfigurationPersistenceContext.tsx +4 -0
- package/src/contexts/index.ts +17 -0
- package/src/core/PluginLifecycleManager.tsx +95 -0
- package/src/core/PluginProviderStack.tsx +40 -0
- package/src/core/Rebase.tsx +439 -0
- package/src/core/RebaseProps.tsx +320 -0
- package/src/core/RebaseRouter.tsx +17 -0
- package/src/core/RebaseRoutes.tsx +24 -0
- package/src/core/index.tsx +5 -0
- package/src/hooks/ApiConfigContext.tsx +41 -0
- package/src/hooks/data/delete.ts +55 -0
- package/src/hooks/data/save.ts +77 -0
- package/src/hooks/data/useCollection.tsx +176 -0
- package/src/hooks/data/useData.tsx +18 -0
- package/src/hooks/data/useDataOrder.ts +26 -0
- package/src/hooks/data/useFetch.tsx +140 -0
- package/src/hooks/data/useRelationSelector.tsx +234 -0
- package/src/hooks/index.tsx +45 -0
- package/src/hooks/useAdminModeController.tsx +23 -0
- package/src/hooks/useAnalyticsController.tsx +8 -0
- package/src/hooks/useAuthController.tsx +14 -0
- package/src/hooks/useAuthSubscription.ts +87 -0
- package/src/hooks/useBackendStorageSource.ts +288 -0
- package/src/hooks/useBridgeRegistration.tsx +32 -0
- package/src/hooks/useBrowserTitleAndIcon.tsx +23 -0
- package/src/hooks/useBuildAdminModeController.tsx +24 -0
- package/src/hooks/useBuildEffectiveRoleController.tsx +31 -0
- package/src/hooks/useBuildLocalConfigurationPersistence.tsx +75 -0
- package/src/hooks/useBuildModeController.tsx +71 -0
- package/src/hooks/useClipboard.tsx +158 -0
- package/src/hooks/useCollapsedGroups.ts +116 -0
- package/src/hooks/useComponentOverride.tsx +59 -0
- package/src/hooks/useCustomizationController.tsx +14 -0
- package/src/hooks/useDialogsController.tsx +15 -0
- package/src/hooks/useEffectiveRoleController.tsx +10 -0
- package/src/hooks/useLargeLayout.tsx +65 -0
- package/src/hooks/useModeController.tsx +23 -0
- package/src/hooks/usePermissions.ts +44 -0
- package/src/hooks/useRebaseClient.tsx +10 -0
- package/src/hooks/useRebaseContext.tsx +83 -0
- package/src/hooks/useRebaseRegistry.tsx +91 -0
- package/src/hooks/useResolvedComponent.tsx +159 -0
- package/src/hooks/useSlot.tsx +64 -0
- package/src/hooks/useSnackbarController.tsx +52 -0
- package/src/hooks/useStorageSource.tsx +11 -0
- package/src/hooks/useStudioBridge.tsx +209 -0
- package/src/hooks/useTranslation.ts +33 -0
- package/src/hooks/useUnsavedChangesDialog.tsx +64 -0
- package/src/hooks/useUserConfigurationPersistence.tsx +11 -0
- package/src/i18n/RebaseI18nProvider.tsx +160 -0
- package/src/index.ts +25 -0
- package/src/internal/common.tsx +16 -0
- package/src/internal/useRestoreScroll.tsx +69 -0
- package/src/locales/de.ts +902 -0
- package/src/locales/en.ts +980 -0
- package/src/locales/es.ts +922 -0
- package/src/locales/fr.ts +901 -0
- package/src/locales/hi.ts +901 -0
- package/src/locales/it.ts +901 -0
- package/src/locales/pt.ts +909 -0
- package/src/util/constants.ts +7 -0
- package/src/util/createFormexStub.tsx +66 -0
- package/src/util/entity_cache.ts +239 -0
- package/src/util/enums.ts +32 -0
- package/src/util/icon_list.ts +22 -0
- package/src/util/icons.tsx +149 -0
- package/src/util/index.ts +12 -0
- package/src/util/previews.ts +105 -0
- package/src/util/useStorageUploadController.tsx +350 -0
- package/src/vitePlugin.ts +196 -0
|
@@ -0,0 +1,932 @@
|
|
|
1
|
+
import React, { ReactNode, useEffect, 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
|
+
cls,
|
|
25
|
+
IconButton,
|
|
26
|
+
iconSize,
|
|
27
|
+
LoadingButton,
|
|
28
|
+
MailIcon,
|
|
29
|
+
Menu,
|
|
30
|
+
MenuItem,
|
|
31
|
+
MoonIcon,
|
|
32
|
+
SunIcon,
|
|
33
|
+
SunMoonIcon,
|
|
34
|
+
TextField,
|
|
35
|
+
Typography
|
|
36
|
+
} from "@rebasepro/ui";
|
|
37
|
+
import { AuthControllerExtended, User } from "@rebasepro/types";
|
|
38
|
+
import { ErrorView } from "../ErrorView";
|
|
39
|
+
import { RebaseLogo } from "../RebaseLogo";
|
|
40
|
+
import { LanguageToggle } from "../LanguageToggle";
|
|
41
|
+
import { useModeController, useTranslation } from "../../hooks";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Props for the generic LoginView.
|
|
45
|
+
* Consumes {@link AuthControllerExtended} — works with any backend
|
|
46
|
+
* (Rebase custom backend, Firebase, etc.)
|
|
47
|
+
*/
|
|
48
|
+
export interface LoginViewProps {
|
|
49
|
+
/**
|
|
50
|
+
* Auth controller — must implement AuthControllerExtended.
|
|
51
|
+
*/
|
|
52
|
+
authController: AuthControllerExtended;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Path to the logo displayed in the login screen
|
|
56
|
+
*/
|
|
57
|
+
logo?: string;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Enable the skip login button
|
|
61
|
+
*/
|
|
62
|
+
allowSkipLogin?: boolean;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Disable the login buttons
|
|
66
|
+
*/
|
|
67
|
+
disabled?: boolean;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Prevent users from creating new accounts.
|
|
71
|
+
* If not set, checks `authController.capabilities.registration`.
|
|
72
|
+
*/
|
|
73
|
+
disableSignupScreen?: boolean;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Display this component when no user is found
|
|
77
|
+
*/
|
|
78
|
+
noUserComponent?: ReactNode;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Display this component below the sign-in buttons
|
|
82
|
+
*/
|
|
83
|
+
additionalComponent?: ReactNode;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Display this component above the sign-in buttons
|
|
87
|
+
*/
|
|
88
|
+
topComponent?: ReactNode;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Error message when user is not allowed access
|
|
92
|
+
*/
|
|
93
|
+
notAllowedError?: string | Error;
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Google client ID for Google OAuth.
|
|
98
|
+
* Required when Google login is enabled via ID token flow.
|
|
99
|
+
*/
|
|
100
|
+
googleClientId?: string;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* GitHub client ID for GitHub OAuth.
|
|
104
|
+
*/
|
|
105
|
+
githubClientId?: string;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* LinkedIn client ID for LinkedIn OAuth.
|
|
109
|
+
*/
|
|
110
|
+
linkedinClientId?: string;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Optional custom title shown above options
|
|
114
|
+
*/
|
|
115
|
+
title?: string;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Optional custom subtitle shown above options
|
|
119
|
+
*/
|
|
120
|
+
subtitle?: string;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* When true, shows bootstrap/setup UI (first-user creation).
|
|
124
|
+
* If not set, derived from `authController` if it exposes `needsSetup`.
|
|
125
|
+
*/
|
|
126
|
+
needsSetup?: boolean;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Whether registration is enabled.
|
|
130
|
+
* If not set, derived from `authController.capabilities.registration`.
|
|
131
|
+
*/
|
|
132
|
+
registrationEnabled?: boolean;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Pre-fill the email field (e.g. for demo or testing environments).
|
|
136
|
+
*/
|
|
137
|
+
defaultEmail?: string;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Pre-fill the password field (e.g. for demo or testing environments).
|
|
141
|
+
*/
|
|
142
|
+
defaultPassword?: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
type AuthMode = "buttons" | "login" | "register" | "forgot";
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The shared field background mixin (`dark:bg-black/30`) is invisible on the
|
|
149
|
+
* near-black login card, so login inputs get an explicit border in dark mode.
|
|
150
|
+
*/
|
|
151
|
+
const loginFieldClasses = "w-full dark:border dark:border-surface-700/70 dark:hover:border-surface-600";
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Generic login view component that works with any AuthControllerExtended.
|
|
155
|
+
* Feature-detects capabilities to show/hide login methods.
|
|
156
|
+
* @group Core
|
|
157
|
+
*/
|
|
158
|
+
export function LoginView({
|
|
159
|
+
logo,
|
|
160
|
+
authController,
|
|
161
|
+
noUserComponent,
|
|
162
|
+
disableSignupScreen = false,
|
|
163
|
+
disabled = false,
|
|
164
|
+
notAllowedError,
|
|
165
|
+
googleClientId,
|
|
166
|
+
githubClientId,
|
|
167
|
+
linkedinClientId,
|
|
168
|
+
title,
|
|
169
|
+
subtitle,
|
|
170
|
+
needsSetup,
|
|
171
|
+
registrationEnabled,
|
|
172
|
+
additionalComponent,
|
|
173
|
+
topComponent,
|
|
174
|
+
defaultEmail,
|
|
175
|
+
defaultPassword
|
|
176
|
+
}: LoginViewProps) {
|
|
177
|
+
|
|
178
|
+
const modeState = useModeController();
|
|
179
|
+
const { mode: colorMode, setMode: setColorMode } = modeState;
|
|
180
|
+
const { t } = useTranslation();
|
|
181
|
+
|
|
182
|
+
const [mode, setMode] = useState<AuthMode>("buttons");
|
|
183
|
+
const [fadeIn, setFadeIn] = useState(false);
|
|
184
|
+
const [viewVisible, setViewVisible] = useState(true);
|
|
185
|
+
|
|
186
|
+
const switchMode = (newMode: AuthMode) => {
|
|
187
|
+
setViewVisible(false);
|
|
188
|
+
setTimeout(() => {
|
|
189
|
+
setMode(newMode);
|
|
190
|
+
setViewVisible(true);
|
|
191
|
+
}, 150);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Resolve capabilities — explicit props override authController.capabilities
|
|
195
|
+
const caps = authController.capabilities ?? {};
|
|
196
|
+
const isBootstrapMode = needsSetup
|
|
197
|
+
?? ("needsSetup" in authController && !!(authController as { needsSetup?: boolean }).needsSetup)
|
|
198
|
+
?? false;
|
|
199
|
+
const canRegister = registrationEnabled ?? caps.registration ?? false;
|
|
200
|
+
const hasGoogleLogin = googleClientId && (caps.enabledProviders?.includes("google") ?? caps.googleLogin ?? false);
|
|
201
|
+
const hasGitHubLogin = githubClientId && (caps.enabledProviders?.includes("github") ?? false);
|
|
202
|
+
const hasLinkedinLogin = linkedinClientId && (caps.enabledProviders?.includes("linkedin") ?? false);
|
|
203
|
+
const hasPasswordReset = caps.passwordReset ?? !!authController.forgotPassword;
|
|
204
|
+
|
|
205
|
+
const showRegistration = !disableSignupScreen && canRegister;
|
|
206
|
+
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
const timer = setTimeout(() => setFadeIn(true), 50);
|
|
209
|
+
return () => clearTimeout(timer);
|
|
210
|
+
}, []);
|
|
211
|
+
|
|
212
|
+
// Effect to handle incoming redirect OAuth codes (GitHub, LinkedIn, etc.)
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
const params = new URLSearchParams(window.location.search);
|
|
215
|
+
const code = params.get("code");
|
|
216
|
+
const provider = localStorage.getItem("rebase_oauth_provider");
|
|
217
|
+
if (code && provider) {
|
|
218
|
+
localStorage.removeItem("rebase_oauth_provider");
|
|
219
|
+
// Clear URL search params without page reload
|
|
220
|
+
const cleanUrl = window.location.origin + window.location.pathname;
|
|
221
|
+
window.history.replaceState({}, document.title, cleanUrl);
|
|
222
|
+
|
|
223
|
+
if (authController.oauthLogin) {
|
|
224
|
+
authController.oauthLogin(provider, {
|
|
225
|
+
code,
|
|
226
|
+
redirectUri: cleanUrl
|
|
227
|
+
}).catch((err) => {
|
|
228
|
+
console.error(`${provider} login failed:`, err);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}, [authController]);
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
let logoComponent;
|
|
237
|
+
if (logo) {
|
|
238
|
+
logoComponent = <img src={logo}
|
|
239
|
+
style={{
|
|
240
|
+
height: "100%",
|
|
241
|
+
width: "100%",
|
|
242
|
+
objectFit: "cover"
|
|
243
|
+
}}
|
|
244
|
+
alt={"Logo"}/>;
|
|
245
|
+
} else {
|
|
246
|
+
logoComponent = <RebaseLogo/>;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let notAllowedMessage: string | undefined;
|
|
250
|
+
if (notAllowedError) {
|
|
251
|
+
if (typeof notAllowedError === "string") {
|
|
252
|
+
notAllowedMessage = notAllowedError;
|
|
253
|
+
} else if (notAllowedError instanceof Error) {
|
|
254
|
+
notAllowedMessage = notAllowedError.message;
|
|
255
|
+
} else {
|
|
256
|
+
notAllowedMessage = "It looks like you don't have access, based on the specified access configuration";
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
<div
|
|
262
|
+
className={cls(
|
|
263
|
+
"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",
|
|
264
|
+
fadeIn ? "opacity-100" : "opacity-0"
|
|
265
|
+
)}>
|
|
266
|
+
|
|
267
|
+
{/* Top-right controls */}
|
|
268
|
+
<div className="absolute top-4 right-4 flex items-center gap-1 z-10">
|
|
269
|
+
<LanguageToggle/>
|
|
270
|
+
<Menu
|
|
271
|
+
trigger={<IconButton
|
|
272
|
+
color="inherit"
|
|
273
|
+
aria-label="Toggle theme">
|
|
274
|
+
{colorMode === "dark"
|
|
275
|
+
? <MoonIcon size={iconSize.small}/>
|
|
276
|
+
: <SunIcon size={iconSize.small}/>}
|
|
277
|
+
</IconButton>}>
|
|
278
|
+
<MenuItem onClick={() => setColorMode("dark")}><MoonIcon size={iconSize.smallest}/> {t("dark_mode")}</MenuItem>
|
|
279
|
+
<MenuItem onClick={() => setColorMode("light")}><SunIcon size={iconSize.smallest}/> {t("light_mode")}</MenuItem>
|
|
280
|
+
<MenuItem onClick={() => setColorMode("system")}><SunMoonIcon size={iconSize.smallest}/> {t("system_mode")}</MenuItem>
|
|
281
|
+
</Menu>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<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">
|
|
285
|
+
{/* Logo */}
|
|
286
|
+
<div className="w-24 h-24 m-2 mb-4 drop-shadow-md">
|
|
287
|
+
{logoComponent}
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
{notAllowedMessage && (
|
|
291
|
+
<div className="p-4 w-full">
|
|
292
|
+
<ErrorView error={notAllowedMessage}/>
|
|
293
|
+
</div>
|
|
294
|
+
)}
|
|
295
|
+
|
|
296
|
+
<div className={cls(
|
|
297
|
+
"w-full transition-opacity duration-150",
|
|
298
|
+
viewVisible ? "opacity-100" : "opacity-0"
|
|
299
|
+
)}>
|
|
300
|
+
|
|
301
|
+
{/* Bootstrap mode: show setup form directly */}
|
|
302
|
+
{isBootstrapMode && !authController.user && (
|
|
303
|
+
<LoginForm
|
|
304
|
+
authController={authController}
|
|
305
|
+
registrationMode={true}
|
|
306
|
+
onClose={() => {}}
|
|
307
|
+
onForgotPassword={() => {}}
|
|
308
|
+
disableSignupScreen={false}
|
|
309
|
+
bootstrapMode={true}
|
|
310
|
+
defaultEmail={defaultEmail}
|
|
311
|
+
defaultPassword={defaultPassword}
|
|
312
|
+
/>
|
|
313
|
+
)}
|
|
314
|
+
|
|
315
|
+
{/* Normal mode */}
|
|
316
|
+
{!isBootstrapMode && (
|
|
317
|
+
<>
|
|
318
|
+
{/* Provider buttons screen */}
|
|
319
|
+
{mode === "buttons" && (
|
|
320
|
+
<div className="w-full flex flex-col gap-3 mt-2">
|
|
321
|
+
{(title || subtitle) && (
|
|
322
|
+
<div className="text-center mb-2">
|
|
323
|
+
{title && (
|
|
324
|
+
<Typography variant="h6" className="mb-0.5 font-bold">
|
|
325
|
+
{title}
|
|
326
|
+
</Typography>
|
|
327
|
+
)}
|
|
328
|
+
{subtitle && (
|
|
329
|
+
<Typography variant="body2" color="secondary" className="mb-4">
|
|
330
|
+
{subtitle}
|
|
331
|
+
</Typography>
|
|
332
|
+
)}
|
|
333
|
+
</div>
|
|
334
|
+
)}
|
|
335
|
+
{topComponent && (
|
|
336
|
+
<div className="w-full">
|
|
337
|
+
{topComponent}
|
|
338
|
+
</div>
|
|
339
|
+
)}
|
|
340
|
+
<LoginButton
|
|
341
|
+
disabled={disabled}
|
|
342
|
+
text={"Sign in with email"}
|
|
343
|
+
icon={<MailIcon/>}
|
|
344
|
+
onClick={() => switchMode("login")}
|
|
345
|
+
/>
|
|
346
|
+
{hasGoogleLogin && googleClientId && (
|
|
347
|
+
<GoogleLoginButton
|
|
348
|
+
disabled={disabled}
|
|
349
|
+
googleClientId={googleClientId}
|
|
350
|
+
authController={authController}
|
|
351
|
+
/>
|
|
352
|
+
)}
|
|
353
|
+
{hasGitHubLogin && githubClientId && (
|
|
354
|
+
<GitHubLoginButton
|
|
355
|
+
disabled={disabled}
|
|
356
|
+
githubClientId={githubClientId}
|
|
357
|
+
/>
|
|
358
|
+
)}
|
|
359
|
+
{hasLinkedinLogin && linkedinClientId && (
|
|
360
|
+
<LinkedInLoginButton
|
|
361
|
+
disabled={disabled}
|
|
362
|
+
linkedinClientId={linkedinClientId}
|
|
363
|
+
/>
|
|
364
|
+
)}
|
|
365
|
+
{showRegistration && (
|
|
366
|
+
<div className="mt-2 text-center">
|
|
367
|
+
<Typography variant="body2" color="secondary">
|
|
368
|
+
Don't have an account?{" "}
|
|
369
|
+
<button
|
|
370
|
+
type="button"
|
|
371
|
+
className="font-semibold hover:underline cursor-pointer text-primary-600 dark:text-primary-400"
|
|
372
|
+
onClick={() => switchMode("register")}
|
|
373
|
+
>
|
|
374
|
+
Create one
|
|
375
|
+
</button>
|
|
376
|
+
</Typography>
|
|
377
|
+
</div>
|
|
378
|
+
)}
|
|
379
|
+
</div>
|
|
380
|
+
)}
|
|
381
|
+
|
|
382
|
+
{/* Login form */}
|
|
383
|
+
{mode === "login" && (
|
|
384
|
+
<LoginForm
|
|
385
|
+
authController={authController}
|
|
386
|
+
registrationMode={false}
|
|
387
|
+
onClose={() => switchMode("buttons")}
|
|
388
|
+
onForgotPassword={hasPasswordReset ? () => switchMode("forgot") : undefined}
|
|
389
|
+
noUserComponent={noUserComponent}
|
|
390
|
+
disableSignupScreen={disableSignupScreen}
|
|
391
|
+
switchToRegister={showRegistration ? () => switchMode("register") : undefined}
|
|
392
|
+
defaultEmail={defaultEmail}
|
|
393
|
+
defaultPassword={defaultPassword}
|
|
394
|
+
/>
|
|
395
|
+
)}
|
|
396
|
+
|
|
397
|
+
{/* Registration form */}
|
|
398
|
+
{mode === "register" && (
|
|
399
|
+
<LoginForm
|
|
400
|
+
authController={authController}
|
|
401
|
+
registrationMode={true}
|
|
402
|
+
onClose={() => switchMode("buttons")}
|
|
403
|
+
onForgotPassword={hasPasswordReset ? () => switchMode("forgot") : undefined}
|
|
404
|
+
noUserComponent={noUserComponent}
|
|
405
|
+
disableSignupScreen={disableSignupScreen}
|
|
406
|
+
switchToLogin={() => switchMode("login")}
|
|
407
|
+
defaultEmail={defaultEmail}
|
|
408
|
+
defaultPassword={defaultPassword}
|
|
409
|
+
/>
|
|
410
|
+
)}
|
|
411
|
+
|
|
412
|
+
{/* Forgot password form */}
|
|
413
|
+
{mode === "forgot" && authController.forgotPassword && (
|
|
414
|
+
<ForgotPasswordForm
|
|
415
|
+
authController={authController}
|
|
416
|
+
onClose={() => switchMode("login")}
|
|
417
|
+
/>
|
|
418
|
+
)}
|
|
419
|
+
</>
|
|
420
|
+
)}
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
{additionalComponent && !isBootstrapMode && mode === "buttons" && (
|
|
424
|
+
<div className="w-full">
|
|
425
|
+
{additionalComponent}
|
|
426
|
+
</div>
|
|
427
|
+
)}
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function LoginButton({
|
|
434
|
+
icon,
|
|
435
|
+
onClick,
|
|
436
|
+
text,
|
|
437
|
+
disabled
|
|
438
|
+
}: { icon: React.ReactNode, onClick: () => void, text: string, disabled?: boolean }) {
|
|
439
|
+
return (
|
|
440
|
+
<Button
|
|
441
|
+
disabled={disabled}
|
|
442
|
+
className="w-full transition-transform duration-200 active:scale-[0.98]"
|
|
443
|
+
variant="outlined"
|
|
444
|
+
size="large"
|
|
445
|
+
onClick={onClick}>
|
|
446
|
+
<div className="flex items-center justify-center w-full gap-3 py-1">
|
|
447
|
+
<span className="flex items-center justify-center w-5 h-5">
|
|
448
|
+
{icon}
|
|
449
|
+
</span>
|
|
450
|
+
<Typography variant="button">{text}</Typography>
|
|
451
|
+
</div>
|
|
452
|
+
</Button>
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const GoogleIcon = () => (
|
|
457
|
+
<svg viewBox="0 0 24 24" width="20" height="20">
|
|
458
|
+
<path fill="#4285F4"
|
|
459
|
+
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"/>
|
|
460
|
+
<path fill="#34A853"
|
|
461
|
+
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"/>
|
|
462
|
+
<path fill="#FBBC05"
|
|
463
|
+
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"/>
|
|
464
|
+
<path fill="#EA4335"
|
|
465
|
+
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"/>
|
|
466
|
+
</svg>
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
function GoogleLoginButton({
|
|
470
|
+
disabled,
|
|
471
|
+
googleClientId,
|
|
472
|
+
authController
|
|
473
|
+
}: {
|
|
474
|
+
disabled?: boolean,
|
|
475
|
+
googleClientId: string,
|
|
476
|
+
authController: AuthControllerExtended
|
|
477
|
+
}) {
|
|
478
|
+
const codeClientRef = useRef<{ requestCode(): void } | null>(null);
|
|
479
|
+
|
|
480
|
+
useEffect(() => {
|
|
481
|
+
if (!authController.googleLogin) return;
|
|
482
|
+
|
|
483
|
+
const google = window.google;
|
|
484
|
+
if (!google || codeClientRef.current) return;
|
|
485
|
+
|
|
486
|
+
codeClientRef.current = google.accounts.oauth2.initCodeClient({
|
|
487
|
+
client_id: googleClientId,
|
|
488
|
+
scope: "openid email profile",
|
|
489
|
+
ux_mode: "popup",
|
|
490
|
+
callback: async (response: { code?: string; error?: string }) => {
|
|
491
|
+
if (response.error || !response.code) {
|
|
492
|
+
console.error("Google login error:", response.error);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
// Send the authorization code to the backend.
|
|
497
|
+
// redirectUri "postmessage" is required when using popup ux_mode.
|
|
498
|
+
await authController.googleLogin!({
|
|
499
|
+
code: response.code,
|
|
500
|
+
redirectUri: "postmessage"
|
|
501
|
+
});
|
|
502
|
+
} catch (err: unknown) {
|
|
503
|
+
console.error("Google login error:", err);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
}, [googleClientId, authController]);
|
|
508
|
+
|
|
509
|
+
const handleClick = () => {
|
|
510
|
+
if (!codeClientRef.current) {
|
|
511
|
+
console.error("Google Sign-In not loaded");
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
codeClientRef.current.requestCode();
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
return (
|
|
518
|
+
<LoginButton
|
|
519
|
+
disabled={disabled}
|
|
520
|
+
text="Sign in with Google"
|
|
521
|
+
icon={<GoogleIcon/>}
|
|
522
|
+
onClick={handleClick}
|
|
523
|
+
/>
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const GitHubIcon = () => (
|
|
528
|
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
|
529
|
+
<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"/>
|
|
530
|
+
</svg>
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
function GitHubLoginButton({
|
|
534
|
+
disabled,
|
|
535
|
+
githubClientId
|
|
536
|
+
}: {
|
|
537
|
+
disabled?: boolean,
|
|
538
|
+
githubClientId: string
|
|
539
|
+
}) {
|
|
540
|
+
const handleClick = () => {
|
|
541
|
+
localStorage.setItem("rebase_oauth_provider", "github");
|
|
542
|
+
const redirectUri = encodeURIComponent(window.location.origin + window.location.pathname);
|
|
543
|
+
const scope = "read:user,user:email";
|
|
544
|
+
window.location.href = `https://github.com/login/oauth/authorize?client_id=${githubClientId}&redirect_uri=${redirectUri}&scope=${scope}`;
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
return (
|
|
548
|
+
<LoginButton
|
|
549
|
+
disabled={disabled}
|
|
550
|
+
text="Sign in with GitHub"
|
|
551
|
+
icon={<GitHubIcon/>}
|
|
552
|
+
onClick={handleClick}
|
|
553
|
+
/>
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const LinkedInIcon = () => (
|
|
558
|
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
|
559
|
+
<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"/>
|
|
560
|
+
</svg>
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
function LinkedInLoginButton({
|
|
564
|
+
disabled,
|
|
565
|
+
linkedinClientId
|
|
566
|
+
}: {
|
|
567
|
+
disabled?: boolean,
|
|
568
|
+
linkedinClientId: string
|
|
569
|
+
}) {
|
|
570
|
+
const handleClick = () => {
|
|
571
|
+
localStorage.setItem("rebase_oauth_provider", "linkedin");
|
|
572
|
+
const redirectUri = encodeURIComponent(window.location.origin + window.location.pathname);
|
|
573
|
+
const scope = "openid profile email";
|
|
574
|
+
window.location.href = `https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${linkedinClientId}&redirect_uri=${redirectUri}&scope=${scope}`;
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
return (
|
|
578
|
+
<LoginButton
|
|
579
|
+
disabled={disabled}
|
|
580
|
+
text="Sign in with LinkedIn"
|
|
581
|
+
icon={<LinkedInIcon/>}
|
|
582
|
+
onClick={handleClick}
|
|
583
|
+
/>
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function LoginForm({
|
|
588
|
+
onClose,
|
|
589
|
+
onForgotPassword,
|
|
590
|
+
authController,
|
|
591
|
+
registrationMode,
|
|
592
|
+
noUserComponent,
|
|
593
|
+
disableSignupScreen,
|
|
594
|
+
bootstrapMode = false,
|
|
595
|
+
switchToRegister,
|
|
596
|
+
switchToLogin,
|
|
597
|
+
defaultEmail,
|
|
598
|
+
defaultPassword
|
|
599
|
+
}: {
|
|
600
|
+
onClose: () => void,
|
|
601
|
+
onForgotPassword?: () => void,
|
|
602
|
+
authController: AuthControllerExtended,
|
|
603
|
+
registrationMode: boolean,
|
|
604
|
+
noUserComponent?: ReactNode,
|
|
605
|
+
disableSignupScreen: boolean,
|
|
606
|
+
bootstrapMode?: boolean,
|
|
607
|
+
switchToRegister?: () => void,
|
|
608
|
+
switchToLogin?: () => void,
|
|
609
|
+
defaultEmail?: string,
|
|
610
|
+
defaultPassword?: string
|
|
611
|
+
}) {
|
|
612
|
+
const passwordRef = useRef<HTMLInputElement | null>(null);
|
|
613
|
+
|
|
614
|
+
const [email, setEmail] = useState<string | undefined>(defaultEmail);
|
|
615
|
+
const [password, setPassword] = useState<string | undefined>(defaultPassword);
|
|
616
|
+
const [displayName, setDisplayName] = useState<string>();
|
|
617
|
+
|
|
618
|
+
useEffect(() => {
|
|
619
|
+
if (!document) return;
|
|
620
|
+
const escFunction = (event: KeyboardEvent) => {
|
|
621
|
+
if (event.key === "Escape") {
|
|
622
|
+
onClose();
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
document.addEventListener("keydown", escFunction, false);
|
|
626
|
+
return () => {
|
|
627
|
+
document.removeEventListener("keydown", escFunction, false);
|
|
628
|
+
};
|
|
629
|
+
}, [onClose]);
|
|
630
|
+
|
|
631
|
+
function handleEnterPassword() {
|
|
632
|
+
if (email && password && authController.emailPasswordLogin) {
|
|
633
|
+
authController.emailPasswordLogin(email, password);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function handleRegistration() {
|
|
638
|
+
if (email && password && authController.register) {
|
|
639
|
+
authController.register(email, password, displayName);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const handleSubmit = (event: React.FormEvent) => {
|
|
644
|
+
event.preventDefault();
|
|
645
|
+
if (registrationMode)
|
|
646
|
+
handleRegistration();
|
|
647
|
+
else
|
|
648
|
+
handleEnterPassword();
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
const title = bootstrapMode
|
|
652
|
+
? "Welcome!"
|
|
653
|
+
: registrationMode
|
|
654
|
+
? "Create account"
|
|
655
|
+
: "Sign in";
|
|
656
|
+
|
|
657
|
+
const subtitle = bootstrapMode
|
|
658
|
+
? "Create your admin account to get started. This account will have admin privileges."
|
|
659
|
+
: registrationMode
|
|
660
|
+
? "Fill in your details to create a new account"
|
|
661
|
+
: "Enter your credentials to continue";
|
|
662
|
+
|
|
663
|
+
const buttonLabel = registrationMode ? "Create account" : "Sign in";
|
|
664
|
+
|
|
665
|
+
return (
|
|
666
|
+
<form onSubmit={handleSubmit} className="flex flex-col w-full gap-1 mt-2">
|
|
667
|
+
{!bootstrapMode && (
|
|
668
|
+
<div className="w-full mb-2 -ml-2.5">
|
|
669
|
+
<IconButton onClick={onClose}>
|
|
670
|
+
<ArrowLeftIcon/>
|
|
671
|
+
</IconButton>
|
|
672
|
+
</div>
|
|
673
|
+
)}
|
|
674
|
+
|
|
675
|
+
{(() => {
|
|
676
|
+
const err = authController.authProviderError;
|
|
677
|
+
if (!err || authController.user) return null;
|
|
678
|
+
const msg: string = err instanceof Error ? err.message : String(err);
|
|
679
|
+
return (
|
|
680
|
+
<div className="w-full mb-2">
|
|
681
|
+
<ErrorView error={msg}/>
|
|
682
|
+
</div>
|
|
683
|
+
);
|
|
684
|
+
})()}
|
|
685
|
+
|
|
686
|
+
<Typography variant="h6" className="mb-0.5">
|
|
687
|
+
{title}
|
|
688
|
+
</Typography>
|
|
689
|
+
<Typography variant="body2" color="secondary" className="mb-5">
|
|
690
|
+
{subtitle}
|
|
691
|
+
</Typography>
|
|
692
|
+
|
|
693
|
+
{registrationMode && !bootstrapMode && noUserComponent && (
|
|
694
|
+
<div className="w-full mb-2">
|
|
695
|
+
{noUserComponent}
|
|
696
|
+
</div>
|
|
697
|
+
)}
|
|
698
|
+
|
|
699
|
+
{registrationMode && (
|
|
700
|
+
<div className="w-full mb-3">
|
|
701
|
+
<Typography variant="label" color="secondary" className="mb-1">
|
|
702
|
+
Display Name
|
|
703
|
+
</Typography>
|
|
704
|
+
<TextField placeholder="Jane Doe (optional)"
|
|
705
|
+
className={loginFieldClasses}
|
|
706
|
+
value={displayName ?? ""}
|
|
707
|
+
disabled={authController.initialLoading}
|
|
708
|
+
type="text"
|
|
709
|
+
size="medium"
|
|
710
|
+
onChange={(event) => setDisplayName(event.target.value)}/>
|
|
711
|
+
</div>
|
|
712
|
+
)}
|
|
713
|
+
|
|
714
|
+
<div className="w-full mb-3">
|
|
715
|
+
<Typography variant="label" color="secondary" className="mb-1">
|
|
716
|
+
Email
|
|
717
|
+
</Typography>
|
|
718
|
+
<TextField placeholder="you@example.com"
|
|
719
|
+
className={loginFieldClasses}
|
|
720
|
+
autoFocus
|
|
721
|
+
value={email ?? ""}
|
|
722
|
+
disabled={authController.initialLoading}
|
|
723
|
+
type="email"
|
|
724
|
+
size="medium"
|
|
725
|
+
onChange={(event) => setEmail(event.target.value)}/>
|
|
726
|
+
</div>
|
|
727
|
+
|
|
728
|
+
<div className="w-full mb-1">
|
|
729
|
+
<Typography variant="label" color="secondary" className="mb-1">
|
|
730
|
+
Password
|
|
731
|
+
</Typography>
|
|
732
|
+
<TextField placeholder="••••••••"
|
|
733
|
+
className={loginFieldClasses}
|
|
734
|
+
value={password ?? ""}
|
|
735
|
+
disabled={authController.initialLoading}
|
|
736
|
+
inputRef={passwordRef}
|
|
737
|
+
type="password"
|
|
738
|
+
size="medium"
|
|
739
|
+
onChange={(event) => setPassword(event.target.value)}/>
|
|
740
|
+
</div>
|
|
741
|
+
|
|
742
|
+
{registrationMode && (
|
|
743
|
+
<Typography variant="caption" color="secondary" className="mb-3">
|
|
744
|
+
Password must be 8+ characters with uppercase, lowercase, and a number
|
|
745
|
+
</Typography>
|
|
746
|
+
)}
|
|
747
|
+
|
|
748
|
+
{!registrationMode && onForgotPassword && (
|
|
749
|
+
<div className="w-full text-right mb-3">
|
|
750
|
+
<button
|
|
751
|
+
type="button"
|
|
752
|
+
className="text-xs font-medium hover:underline cursor-pointer text-primary-600 dark:text-primary-400"
|
|
753
|
+
onClick={onForgotPassword}
|
|
754
|
+
>
|
|
755
|
+
Forgot password?
|
|
756
|
+
</button>
|
|
757
|
+
</div>
|
|
758
|
+
)}
|
|
759
|
+
|
|
760
|
+
<LoadingButton
|
|
761
|
+
type="submit"
|
|
762
|
+
variant="filled"
|
|
763
|
+
color="primary"
|
|
764
|
+
className="w-full mt-1"
|
|
765
|
+
size="large"
|
|
766
|
+
loading={authController.authLoading}
|
|
767
|
+
disabled={authController.authLoading || !email || !password}
|
|
768
|
+
>
|
|
769
|
+
{buttonLabel}
|
|
770
|
+
</LoadingButton>
|
|
771
|
+
|
|
772
|
+
{/* Switch between login/register */}
|
|
773
|
+
{switchToRegister && (
|
|
774
|
+
<div className="mt-4 text-center">
|
|
775
|
+
<Typography variant="body2" color="secondary">
|
|
776
|
+
Don't have an account?{" "}
|
|
777
|
+
<button
|
|
778
|
+
type="button"
|
|
779
|
+
className="font-semibold hover:underline cursor-pointer text-primary-600 dark:text-primary-400"
|
|
780
|
+
onClick={switchToRegister}
|
|
781
|
+
>
|
|
782
|
+
Create one
|
|
783
|
+
</button>
|
|
784
|
+
</Typography>
|
|
785
|
+
</div>
|
|
786
|
+
)}
|
|
787
|
+
|
|
788
|
+
{switchToLogin && (
|
|
789
|
+
<div className="mt-4 text-center">
|
|
790
|
+
<Typography variant="body2" color="secondary">
|
|
791
|
+
Already have an account?{" "}
|
|
792
|
+
<button
|
|
793
|
+
type="button"
|
|
794
|
+
className="font-semibold hover:underline cursor-pointer text-primary-600 dark:text-primary-400"
|
|
795
|
+
onClick={switchToLogin}
|
|
796
|
+
>
|
|
797
|
+
Sign in
|
|
798
|
+
</button>
|
|
799
|
+
</Typography>
|
|
800
|
+
</div>
|
|
801
|
+
)}
|
|
802
|
+
</form>
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function ForgotPasswordForm({
|
|
807
|
+
onClose,
|
|
808
|
+
authController
|
|
809
|
+
}: {
|
|
810
|
+
onClose: () => void,
|
|
811
|
+
authController: AuthControllerExtended
|
|
812
|
+
}) {
|
|
813
|
+
const [email, setEmail] = useState<string>("");
|
|
814
|
+
const [submitted, setSubmitted] = useState(false);
|
|
815
|
+
const [error, setError] = useState<string | null>(null);
|
|
816
|
+
|
|
817
|
+
useEffect(() => {
|
|
818
|
+
if (!document) return;
|
|
819
|
+
const escFunction = (event: KeyboardEvent) => {
|
|
820
|
+
if (event.key === "Escape") {
|
|
821
|
+
onClose();
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
document.addEventListener("keydown", escFunction, false);
|
|
825
|
+
return () => {
|
|
826
|
+
document.removeEventListener("keydown", escFunction, false);
|
|
827
|
+
};
|
|
828
|
+
}, [onClose]);
|
|
829
|
+
|
|
830
|
+
const handleSubmit = async (event: React.FormEvent) => {
|
|
831
|
+
event.preventDefault();
|
|
832
|
+
setError(null);
|
|
833
|
+
|
|
834
|
+
if (!email) {
|
|
835
|
+
setError("Please enter your email address");
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (!authController.forgotPassword) {
|
|
840
|
+
setError("Password reset is not available with this auth provider.");
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
try {
|
|
845
|
+
await authController.forgotPassword(email);
|
|
846
|
+
setSubmitted(true);
|
|
847
|
+
} catch (err: unknown) {
|
|
848
|
+
// Check for EMAIL_NOT_CONFIGURED error
|
|
849
|
+
if (err instanceof Error && (err as { code?: string }).code === "EMAIL_NOT_CONFIGURED") {
|
|
850
|
+
setError("Password reset is not available. Please contact your administrator.");
|
|
851
|
+
} else {
|
|
852
|
+
// Still show success (security: don't reveal if email exists)
|
|
853
|
+
setSubmitted(true);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
if (submitted) {
|
|
859
|
+
return (
|
|
860
|
+
<div className="flex flex-col w-full gap-4 mt-2">
|
|
861
|
+
<div className="w-full -ml-2.5">
|
|
862
|
+
<IconButton onClick={onClose}>
|
|
863
|
+
<ArrowLeftIcon/>
|
|
864
|
+
</IconButton>
|
|
865
|
+
</div>
|
|
866
|
+
|
|
867
|
+
<div className="text-center rounded-xl p-6 bg-surface-50 dark:bg-surface-900/60 dark:border dark:border-surface-700/50">
|
|
868
|
+
<div className="text-3xl mb-3">📧</div>
|
|
869
|
+
<Typography variant="subtitle1" className="mb-2">
|
|
870
|
+
Check your email
|
|
871
|
+
</Typography>
|
|
872
|
+
<Typography variant="body2" color="secondary">
|
|
873
|
+
If an account exists for <strong>{email}</strong>, you'll receive a password reset link shortly.
|
|
874
|
+
</Typography>
|
|
875
|
+
</div>
|
|
876
|
+
|
|
877
|
+
<Button onClick={onClose} variant="text" className="mt-2">
|
|
878
|
+
Back to sign in
|
|
879
|
+
</Button>
|
|
880
|
+
</div>
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
return (
|
|
885
|
+
<form onSubmit={handleSubmit} className="flex flex-col w-full gap-1 mt-2">
|
|
886
|
+
<div className="w-full mb-2 -ml-2.5">
|
|
887
|
+
<IconButton onClick={onClose}>
|
|
888
|
+
<ArrowLeftIcon/>
|
|
889
|
+
</IconButton>
|
|
890
|
+
</div>
|
|
891
|
+
|
|
892
|
+
<Typography variant="h6" className="mb-0.5">
|
|
893
|
+
Reset password
|
|
894
|
+
</Typography>
|
|
895
|
+
<Typography variant="body2" color="secondary" className="mb-5">
|
|
896
|
+
Enter your email and we'll send you a reset link.
|
|
897
|
+
</Typography>
|
|
898
|
+
|
|
899
|
+
{error && (
|
|
900
|
+
<div className="w-full mb-3">
|
|
901
|
+
<ErrorView error={error}/>
|
|
902
|
+
</div>
|
|
903
|
+
)}
|
|
904
|
+
|
|
905
|
+
<div className="w-full mb-3">
|
|
906
|
+
<Typography variant="label" color="secondary" className="mb-1">
|
|
907
|
+
Email
|
|
908
|
+
</Typography>
|
|
909
|
+
<TextField
|
|
910
|
+
placeholder="you@example.com"
|
|
911
|
+
className={loginFieldClasses}
|
|
912
|
+
autoFocus
|
|
913
|
+
value={email}
|
|
914
|
+
type="email"
|
|
915
|
+
size="medium"
|
|
916
|
+
onChange={(event) => setEmail(event.target.value)}
|
|
917
|
+
/>
|
|
918
|
+
</div>
|
|
919
|
+
|
|
920
|
+
<LoadingButton
|
|
921
|
+
type="submit"
|
|
922
|
+
variant="filled"
|
|
923
|
+
className="w-full"
|
|
924
|
+
size="large"
|
|
925
|
+
loading={authController.authLoading}
|
|
926
|
+
disabled={authController.authLoading || !email}
|
|
927
|
+
>
|
|
928
|
+
Send reset link
|
|
929
|
+
</LoadingButton>
|
|
930
|
+
</form>
|
|
931
|
+
);
|
|
932
|
+
}
|