@micha.bigler/ui-core-micha 3.14.3 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +7 -0
- package/dist/auth/UserMenu.js +1 -1
- package/dist/auth/apiClient.js +7 -6
- package/dist/chat/ChatList.js +1 -1
- package/dist/chat/ChatThread.js +2 -1
- package/dist/chat/ChatWorkspace.js +1 -1
- package/dist/chat/ModelPicker.js +2 -5
- package/dist/chat/kitSxRegistry.js +9 -0
- package/dist/chat/useChatSocket.js +1 -1
- package/dist/components/LoginForm.js +1 -1
- package/dist/components/SecurityComponent.js +1 -1
- package/dist/components/UserListComponent.js +2 -8
- package/dist/components/charts/ChartFrame.js +1 -1
- package/dist/components/charts/ScatterChart.js +2 -1
- package/dist/components/charts/kitSxRegistry.js +6 -0
- package/dist/entries/charts.js +6 -0
- package/dist/entries/chat.js +8 -0
- package/dist/entries/markdown.js +1 -0
- package/dist/index.js +0 -14
- package/dist/layout/SectionNav.js +1 -1
- package/dist/messaging/AttachmentList.js +2 -2
- package/dist/messaging/Composer.js +2 -1
- package/dist/messaging/ConversationLaunchers.js +1 -4
- package/dist/messaging/ConversationList.js +1 -1
- package/dist/messaging/DirectMessageLauncher.js +1 -1
- package/dist/messaging/MessagingProvider.js +0 -0
- package/dist/messaging/PollCard.js +7 -5
- package/dist/messaging/ReadTicks.js +2 -2
- package/dist/messaging/Thread.js +5 -3
- package/dist/notifications/NotificationBell.js +4 -7
- package/dist/notifications/NotificationSettings.js +1 -1
- package/dist/onboarding/OnboardingProvider.js +3 -6
- package/dist/pages/AccountPage.js +1 -2
- package/dist/pages/LoginPage.js +2 -3
- package/dist/pages/PasswordChangePage.js +2 -3
- package/dist/pages/PasswordInvitePage.js +2 -3
- package/dist/pages/PasswordResetRequestPage.js +2 -3
- package/dist/pages/SignUpPage.js +2 -3
- package/dist/pages/SignupConfirmPage.js +2 -3
- package/dist/theme/createAppTheme.js +5 -4
- package/dist/theme/kitSxEntry.js +11 -0
- package/dist/theme/kitSxRegistry.js +2 -15
- package/dist/theme/themeCompleteness.js +3 -12
- package/dist/utils/auth-errors.js +4 -3
- package/package.json +41 -16
- package/.github/workflows/publish.yml +0 -126
- package/DESIGN.md +0 -60
- package/WORK_ORDERS.md +0 -148
- package/dev/entries.jsx +0 -721
- package/dev/fixtures.js +0 -26
- package/dev/main.jsx +0 -75
- package/dev/mockTransport.jsx +0 -128
- package/docs/CHART-LAYOUT.md +0 -157
- package/docs/DARK-TOKENS.md +0 -140
- package/docs/TYPE-SCALE.md +0 -111
- package/docs/messaging-deviations.md +0 -118
- package/index.html +0 -12
- package/renovate.json +0 -10
- package/scripts/chart-api-census.mjs +0 -415
- package/src/auth/AuthContext.jsx +0 -158
- package/src/auth/ReauthModal.jsx +0 -82
- package/src/auth/UserMenu.jsx +0 -121
- package/src/auth/apiClient.jsx +0 -232
- package/src/auth/authApi.jsx +0 -539
- package/src/auth/authConfig.jsx +0 -34
- package/src/auth/reauth.js +0 -47
- package/src/chat/ChatComposer.jsx +0 -36
- package/src/chat/ChatList.jsx +0 -87
- package/src/chat/ChatThread.jsx +0 -128
- package/src/chat/ChatWorkspace.jsx +0 -244
- package/src/chat/MarkdownReply.jsx +0 -22
- package/src/chat/ModelPicker.jsx +0 -85
- package/src/chat/api.js +0 -18
- package/src/chat/useChatSocket.js +0 -32
- package/src/chat/wire.js +0 -7
- package/src/components/AccessCodeManager.jsx +0 -288
- package/src/components/AccessCodeSingleUseToggle.jsx +0 -92
- package/src/components/AllowedEmailDomainsManager.jsx +0 -92
- package/src/components/AuthFactorRequirementCard.jsx +0 -95
- package/src/components/BulkInviteCsvTab.jsx +0 -198
- package/src/components/EmptyState.jsx +0 -38
- package/src/components/LoginForm.jsx +0 -159
- package/src/components/MFAComponent.jsx +0 -350
- package/src/components/MfaLoginComponent.jsx +0 -252
- package/src/components/MobileBottomNav.jsx +0 -82
- package/src/components/PasskeysComponent.jsx +0 -252
- package/src/components/PasswordChangeForm.jsx +0 -87
- package/src/components/PasswordResetRequestForm.jsx +0 -43
- package/src/components/PasswordSetForm.jsx +0 -97
- package/src/components/ProfileComponent.jsx +0 -348
- package/src/components/QrSignupManager.jsx +0 -404
- package/src/components/QrSignupValidityManager.jsx +0 -106
- package/src/components/RegistrationMethodsManager.jsx +0 -150
- package/src/components/SecurityComponent.jsx +0 -141
- package/src/components/SocialLoginButtons.jsx +0 -83
- package/src/components/SoftChip.jsx +0 -93
- package/src/components/StatTile.jsx +0 -39
- package/src/components/SupportRecoveryRequestsTab.jsx +0 -482
- package/src/components/TurnstileWidget.jsx +0 -99
- package/src/components/UserInviteComponent.jsx +0 -76
- package/src/components/UserListComponent.jsx +0 -618
- package/src/components/charts/BarChart.jsx +0 -102
- package/src/components/charts/ChartFrame.jsx +0 -214
- package/src/components/charts/LineChart.jsx +0 -126
- package/src/components/charts/ScatterChart.jsx +0 -299
- package/src/components/charts/TimeSeriesChart.jsx +0 -300
- package/src/components/charts/chartDefaults.js +0 -644
- package/src/components/charts/chartLabels.js +0 -36
- package/src/components/charts/exportChart.js +0 -258
- package/src/components/charts/formatters.js +0 -39
- package/src/components/charts/palette.js +0 -33
- package/src/components/charts/yearTickInterval.js +0 -7
- package/src/i18n/authTranslations.ts +0 -2651
- package/src/i18n/chartsTranslations.ts +0 -91
- package/src/i18n/chatTranslations.ts +0 -55
- package/src/i18n/messagingTranslations.ts +0 -120
- package/src/i18n/notificationsTranslations.ts +0 -32
- package/src/i18n/onboardingTranslations.ts +0 -29
- package/src/i18n/sectionNavTranslations.ts +0 -20
- package/src/i18n/uiCoreTranslations.js +0 -29
- package/src/i18n/userMenuTranslations.ts +0 -5
- package/src/index.js +0 -158
- package/src/layout/PageLayout.jsx +0 -36
- package/src/layout/SectionNav.jsx +0 -320
- package/src/messaging/AttachmentList.jsx +0 -145
- package/src/messaging/Composer.jsx +0 -197
- package/src/messaging/ConversationLaunchers.jsx +0 -51
- package/src/messaging/ConversationList.jsx +0 -109
- package/src/messaging/DirectMessageLauncher.jsx +0 -112
- package/src/messaging/MessageBubble.jsx +0 -183
- package/src/messaging/MessagingProvider.jsx +0 -0
- package/src/messaging/MessagingScopeConfig.jsx +0 -24
- package/src/messaging/PollCard.jsx +0 -163
- package/src/messaging/ReactionBar.jsx +0 -43
- package/src/messaging/ReadTicks.jsx +0 -82
- package/src/messaging/Thread.jsx +0 -195
- package/src/messaging/api.js +0 -48
- package/src/messaging/composerImageCompression.js +0 -35
- package/src/messaging/linkifyText.jsx +0 -37
- package/src/notifications/NotificationBell.jsx +0 -84
- package/src/notifications/NotificationSettings.jsx +0 -330
- package/src/notifications/NotificationsProvider.jsx +0 -224
- package/src/notifications/PopupSurface.jsx +0 -105
- package/src/notifications/api.js +0 -55
- package/src/notifications/feedApi.js +0 -18
- package/src/notifications/realtime.jsx +0 -138
- package/src/notifications/serviceWorker/sw.js +0 -50
- package/src/onboarding/OnboardingProvider.jsx +0 -326
- package/src/onboarding/OnboardingWizard.jsx +0 -61
- package/src/onboarding/WizardDialogShell.jsx +0 -51
- package/src/onboarding/api.js +0 -13
- package/src/onboarding/stepSelection.js +0 -16
- package/src/onboarding/steps/BrowserPushStep.jsx +0 -121
- package/src/onboarding/steps/CompleteNameStep.jsx +0 -46
- package/src/onboarding/steps/CookieConsentStep.jsx +0 -133
- package/src/onboarding/steps/PwaInstallStep.jsx +0 -47
- package/src/pages/AccountPage.jsx +0 -394
- package/src/pages/LoginPage.jsx +0 -343
- package/src/pages/PasswordChangePage.jsx +0 -62
- package/src/pages/PasswordInvitePage.jsx +0 -177
- package/src/pages/PasswordResetRequestPage.jsx +0 -68
- package/src/pages/SignUpPage.jsx +0 -268
- package/src/pages/SignupConfirmPage.jsx +0 -155
- package/src/testing/checkKitIntegration.js +0 -233
- package/src/theme/createAppTheme.js +0 -423
- package/src/theme/fonts.js +0 -3
- package/src/theme/index.js +0 -21
- package/src/theme/kitSxRegistry.js +0 -285
- package/src/theme/themeCompleteness.js +0 -1045
- package/src/theme/tokens.js +0 -478
- package/src/utils/auth-errors.js +0 -76
- package/src/utils/authService.js +0 -223
- package/src/utils/resolveErrorText.js +0 -28
- package/src/utils/webauthn.js +0 -51
- package/tests/AccountPage.profileStatements.test.jsx +0 -162
- package/tests/AccountPage.test.jsx +0 -200
- package/tests/AuthContext.test.jsx +0 -129
- package/tests/BarChart.test.jsx +0 -112
- package/tests/BrowserPushStep.test.jsx +0 -110
- package/tests/ChartFrame.test.jsx +0 -336
- package/tests/ConversationList.test.jsx +0 -144
- package/tests/CookieConsentStep.test.jsx +0 -69
- package/tests/EmptyState.test.jsx +0 -62
- package/tests/LineChart.test.jsx +0 -80
- package/tests/MessagingProvider.test.jsx +0 -50
- package/tests/MobileBottomNav.test.jsx +0 -319
- package/tests/NotificationBell.test.jsx +0 -60
- package/tests/NotificationSettings.test.jsx +0 -276
- package/tests/NotificationsProvider.test.jsx +0 -240
- package/tests/OnboardingProvider.test.jsx +0 -294
- package/tests/OnboardingWizard.test.jsx +0 -143
- package/tests/PasswordChangeForm.test.jsx +0 -70
- package/tests/PasswordInvitePage.test.jsx +0 -142
- package/tests/PasswordSetForm.test.jsx +0 -70
- package/tests/PopupSurface.test.jsx +0 -142
- package/tests/ProfileComponent.refresh.test.jsx +0 -194
- package/tests/QrSignupManager.registrationContext.test.jsx +0 -106
- package/tests/QrSignupManagerInfoBox.test.jsx +0 -49
- package/tests/ScatterChart.test.jsx +0 -270
- package/tests/SectionNav.test.jsx +0 -363
- package/tests/SignUpPage.test.jsx +0 -208
- package/tests/SoftChip.test.jsx +0 -105
- package/tests/StatTile.test.jsx +0 -57
- package/tests/Thread.test.jsx +0 -151
- package/tests/TimeSeriesChart.test.jsx +0 -367
- package/tests/UserListComponent.test.jsx +0 -192
- package/tests/UserMenu.test.jsx +0 -171
- package/tests/apiClientLanguage.test.js +0 -33
- package/tests/apiClientPublicPaths.test.js +0 -178
- package/tests/authApiLogin.test.js +0 -42
- package/tests/authApiRegistration.test.js +0 -31
- package/tests/authErrorCodeRendering.test.jsx +0 -77
- package/tests/authErrors.test.js +0 -74
- package/tests/authTranslations.test.js +0 -74
- package/tests/chart-api-census.test.js +0 -366
- package/tests/chartAxisTickBand.test.jsx +0 -95
- package/tests/chartDefaults.test.js +0 -902
- package/tests/chartLabels.test.js +0 -50
- package/tests/chartsPalette.test.js +0 -99
- package/tests/chatApi.test.js +0 -48
- package/tests/chatDriverNames.test.jsx +0 -71
- package/tests/chatExports.test.js +0 -3
- package/tests/chatList.test.jsx +0 -84
- package/tests/chatPicker.test.jsx +0 -82
- package/tests/chatSocket.test.jsx +0 -26
- package/tests/chatThread.test.jsx +0 -116
- package/tests/chatTranslations.test.js +0 -47
- package/tests/chatWorkspace.test.jsx +0 -234
- package/tests/checkKitIntegration.test.jsx +0 -219
- package/tests/createAppTheme.test.js +0 -132
- package/tests/createAppThemeDark.test.js +0 -127
- package/tests/createAppThemeLightGuard.test.js +0 -40
- package/tests/createAppThemePageContrast.test.js +0 -108
- package/tests/createAppThemeTypography.test.js +0 -53
- package/tests/errorCallSiteGuard.test.js +0 -32
- package/tests/exportChart.test.js +0 -313
- package/tests/feedApi.test.js +0 -46
- package/tests/fixtures/light-theme-baseline.json +0 -2234
- package/tests/i18nAggregate.test.js +0 -120
- package/tests/i18nGermanVariant.test.js +0 -65
- package/tests/loginPageErrorResolution.test.jsx +0 -102
- package/tests/messagingApi.test.js +0 -32
- package/tests/messagingChunk6.test.jsx +0 -116
- package/tests/messagingCompactBubble.test.jsx +0 -64
- package/tests/messagingComposer.test.jsx +0 -136
- package/tests/messagingComposerChunk5.test.jsx +0 -113
- package/tests/messagingContractConformance.test.js +0 -69
- package/tests/messagingDirectMessageLauncher.test.jsx +0 -151
- package/tests/messagingExports.test.js +0 -21
- package/tests/messagingInteractions.test.jsx +0 -136
- package/tests/messagingLastMessagePreview.test.jsx +0 -93
- package/tests/messagingLinkify.test.jsx +0 -51
- package/tests/messagingMessageActions.test.jsx +0 -163
- package/tests/messagingMsg6.test.jsx +0 -105
- package/tests/messagingMsg6f.test.jsx +0 -155
- package/tests/messagingMsg6g.test.jsx +0 -166
- package/tests/messagingMsg6h.test.jsx +0 -136
- package/tests/messagingMsg6i.test.jsx +0 -129
- package/tests/messagingPollRendering.test.jsx +0 -142
- package/tests/messagingReactionMerge.test.jsx +0 -77
- package/tests/messagingReadTicksTooltip.test.jsx +0 -36
- package/tests/messagingThreadReceipts.test.jsx +0 -178
- package/tests/messagingThreadingQuoting.test.jsx +0 -87
- package/tests/messagingUnreadLifecycle.test.jsx +0 -78
- package/tests/notificationsApi.test.js +0 -41
- package/tests/notificationsExports.test.js +0 -34
- package/tests/onboardingDescriptors.test.js +0 -132
- package/tests/packageTreeShaking.test.js +0 -126
- package/tests/realtime.test.jsx +0 -219
- package/tests/resolveErrorText.test.js +0 -72
- package/tests/stepSelection.test.js +0 -53
- package/tests/themeCompleteness.test.js +0 -626
- package/tests/timeSeriesLegend.test.js +0 -99
- package/tests/userScreenTranslations.test.jsx +0 -218
- package/tsconfig.build.json +0 -18
- package/vite.config.mjs +0 -6
- package/work-orders/AUTH-1.md +0 -74
- package/work-orders/AUTH-2.md +0 -74
- package/work-orders/AUTH-4.md +0 -183
- package/work-orders/AUTH-5.md +0 -154
- package/work-orders/AUTH-7.md +0 -181
- package/work-orders/AUTH-8.md +0 -184
- package/work-orders/CHART-1.md +0 -285
- package/work-orders/CHART-2.md +0 -222
- package/work-orders/CHART-3.md +0 -163
- package/work-orders/CHART-4.md +0 -203
- package/work-orders/CHART-5.md +0 -226
- package/work-orders/CHART-6.md +0 -428
- package/work-orders/CHART-7.md +0 -175
- package/work-orders/DX-1.md +0 -169
- package/work-orders/DX-2.md +0 -188
- package/work-orders/DX-3.md +0 -195
- package/work-orders/I18N-1.md +0 -254
- package/work-orders/I18N-3.md +0 -207
- package/work-orders/MSG-3.md +0 -375
- package/work-orders/MSG-3b.md +0 -1002
- package/work-orders/MSG-3c.md +0 -378
- package/work-orders/MSG-3d.md +0 -268
- package/work-orders/MSG-3e.md +0 -164
- package/work-orders/MSG-3f.md +0 -391
- package/work-orders/MSG-6e.md +0 -308
- package/work-orders/MSG-6f.md +0 -214
- package/work-orders/MSG-6g.md +0 -244
- package/work-orders/MSG-6h.md +0 -238
- package/work-orders/MSG-6i.md +0 -271
- package/work-orders/MSG-6j.md +0 -117
- package/work-orders/NOTIF-12.md +0 -214
- package/work-orders/NOTIF-13.md +0 -142
- package/work-orders/NOTIF-14.md +0 -151
- package/work-orders/PRIM-1.md +0 -177
- package/work-orders/SHELL-1.md +0 -255
- package/work-orders/SHELL-2.md +0 -197
- package/work-orders/SHELL-3.md +0 -359
- package/work-orders/SHELL-4.md +0 -351
- package/work-orders/SHELL-5.md +0 -345
- package/work-orders/SHELL-6.md +0 -210
- package/work-orders/THEME-1.md +0 -587
- package/work-orders/THEME-10.md +0 -303
- package/work-orders/THEME-11.md +0 -208
- package/work-orders/THEME-2.md +0 -339
- package/work-orders/THEME-3.md +0 -210
- package/work-orders/THEME-4.md +0 -271
- package/work-orders/THEME-5.md +0 -341
- package/work-orders/THEME-6.md +0 -247
- package/work-orders/THEME-7.md +0 -82
- package/work-orders/THEME-8.md +0 -211
- package/work-orders/THEME-9.md +0 -194
- package/work-orders/UCM-AUTH-10.md +0 -490
- package/work-orders/UCM-AUTH-11.md +0 -185
- package/work-orders/UCM-AUTH-12.md +0 -98
- package/work-orders/UCM-AUTH-13.md +0 -47
- package/work-orders/UCM-AUTH-9.md +0 -207
- package/work-orders/UCM-CHART-10.md +0 -150
- package/work-orders/UCM-CHART-11.md +0 -132
- package/work-orders/UCM-CHART-12.md +0 -348
- package/work-orders/UCM-CHART-13.md +0 -122
- package/work-orders/UCM-CHART-14.md +0 -198
- package/work-orders/UCM-CHART-15.md +0 -187
- package/work-orders/UCM-CHART-16.md +0 -169
- package/work-orders/UCM-CHART-17.md +0 -195
- package/work-orders/UCM-CHART-18.md +0 -200
- package/work-orders/UCM-CHART-8.md +0 -147
- package/work-orders/UCM-CHART-9.md +0 -265
- package/work-orders/UCM-CHAT-1.md +0 -190
- package/work-orders/UCM-CHAT-2.md +0 -75
- package/work-orders/UCM-DEP-1.md +0 -63
- package/work-orders/UCM-I18N-4.md +0 -226
- package/work-orders/UCM-I18N-5.md +0 -211
- package/work-orders/UCM-MSG-20.md +0 -192
- package/work-orders/UCM-PRIM-2.md +0 -114
- package/work-orders/UCM-SHELL-7.md +0 -134
- package/work-orders/UCM-SHELL-8.md +0 -125
- package/work-orders/UCM-TEST-1.md +0 -304
- package/work-orders/UCM-THEME-13.md +0 -69
- package/work-orders/UCM-THEME-14.md +0 -312
- package/work-orders/UCM-THEME-15.md +0 -356
- package/work-orders/UCM-THEME-16.md +0 -80
- package/work-orders/UCM-THEME-17.md +0 -111
- package/work-orders/UCM-THEME-18.md +0 -184
- package/work-orders/assets/AUTH-4-user-list-card.html +0 -407
- package/work-orders/assets/PRIM-1-dashboard-primitives.html +0 -624
- package/work-orders/assets/SHELL-2-account-navigation.html +0 -1159
- package/work-orders/assets/THEME-1-baseline-reference-sheet.html +0 -1352
- package/work-orders/assets/UCM-CHAT-1-ai-chat.html +0 -447
- package/work-orders/assets/UCM-THEME-13-dark-prototype.html +0 -390
- package/work-orders/assets/UCM-THEME-17-type-scale.html +0 -391
package/src/pages/LoginPage.jsx
DELETED
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
import React, { useState, useContext, useEffect } from 'react';
|
|
2
|
-
import { useNavigate, useLocation } from 'react-router-dom';
|
|
3
|
-
import { Helmet } from 'react-helmet';
|
|
4
|
-
import { Box, Alert } from '@mui/material';
|
|
5
|
-
import { useTranslation } from 'react-i18next';
|
|
6
|
-
|
|
7
|
-
// Layout & Context
|
|
8
|
-
import { NarrowPage } from '../layout/PageLayout';
|
|
9
|
-
import { AuthContext } from '../auth/AuthContext';
|
|
10
|
-
|
|
11
|
-
// API & Services (Clean Architecture)
|
|
12
|
-
import {
|
|
13
|
-
fetchRecoverySessionToken,
|
|
14
|
-
loginWithPassword,
|
|
15
|
-
loginWithRecoveryPassword,
|
|
16
|
-
} from '../auth/authApi';
|
|
17
|
-
import { loginWithPasskey, startSocialLogin } from '../utils/authService';
|
|
18
|
-
|
|
19
|
-
// Components
|
|
20
|
-
import { LoginForm } from '../components/LoginForm';
|
|
21
|
-
import { MfaLoginComponent } from '../components/MfaLoginComponent';
|
|
22
|
-
import { resolveErrorText } from '../utils/resolveErrorText';
|
|
23
|
-
|
|
24
|
-
export const LOGIN_PAGE_ALERT_SX = { mb: 2 };
|
|
25
|
-
|
|
26
|
-
export function LoginPage() {
|
|
27
|
-
const navigate = useNavigate();
|
|
28
|
-
const location = useLocation();
|
|
29
|
-
const { user, loading, login, authMethods } = useContext(AuthContext);
|
|
30
|
-
const { t, i18n } = useTranslation();
|
|
31
|
-
|
|
32
|
-
// State
|
|
33
|
-
const [step, setStep] = useState('credentials'); // 'credentials' | 'mfa'
|
|
34
|
-
const [submitting, setSubmitting] = useState(false);
|
|
35
|
-
const [errorKey, setErrorKey] = useState(null);
|
|
36
|
-
const [mfaState, setMfaState] = useState(null); // { availableTypes: [...], identifier }
|
|
37
|
-
// S164: token never travels through the URL/fragment anymore. It is held
|
|
38
|
-
// in component state only, fetched from the server-side session handoff
|
|
39
|
-
// endpoint when the redirect lands the SPA here with `#recovery=ok`.
|
|
40
|
-
const [recoveryToken, setRecoveryToken] = useState(null);
|
|
41
|
-
const [recoveryFetching, setRecoveryFetching] = useState(false);
|
|
42
|
-
|
|
43
|
-
// URL Params parsing
|
|
44
|
-
const params = new URLSearchParams(location.search);
|
|
45
|
-
const hashParams = new URLSearchParams(
|
|
46
|
-
String(location.hash || "").startsWith("#") ? String(location.hash).slice(1) : String(location.hash || ""),
|
|
47
|
-
);
|
|
48
|
-
// After S164 the hash carries only a status sentinel: `ok`, `invalid`, or
|
|
49
|
-
// `expired`. We also accept the same key from query params as a defensive
|
|
50
|
-
// fallback for proxies that strip fragments.
|
|
51
|
-
const recoveryStatus = String(
|
|
52
|
-
hashParams.get('recovery') || params.get('recovery') || '',
|
|
53
|
-
).toLowerCase();
|
|
54
|
-
// Backward-compatible fallback for legacy links using query parameters.
|
|
55
|
-
const recoveryEmail = hashParams.get('email') || params.get('email') || '';
|
|
56
|
-
const requestedNext = params.get('next');
|
|
57
|
-
|
|
58
|
-
const getRedirectTarget = (currentUser, options = {}) => {
|
|
59
|
-
if (options.forceSecurityRedirect) {
|
|
60
|
-
return options.forceSecurityRedirect;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const requiresExtra = currentUser?.security_state?.requires_additional_security === true;
|
|
64
|
-
if (requiresExtra) {
|
|
65
|
-
return '/account?tab=security&from=weak_login';
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Same-origin check via URL parser — `startsWith('/')` alone allows
|
|
69
|
-
// bypasses like `/\evil.com`, which WHATWG-parses to `https://evil.com/`
|
|
70
|
-
// for special schemes. Resolve against the current origin and only accept
|
|
71
|
-
// results whose origin matches.
|
|
72
|
-
// Require leading-slash absolute path on the raw input (rejects relative
|
|
73
|
-
// inputs like `account/settings` and protocol-relative `//host` before
|
|
74
|
-
// they reach the URL parser, which would normalise both into a same-
|
|
75
|
-
// origin pathname starting with `/`).
|
|
76
|
-
if (
|
|
77
|
-
requestedNext &&
|
|
78
|
-
typeof requestedNext === 'string' &&
|
|
79
|
-
requestedNext.startsWith('/') &&
|
|
80
|
-
!requestedNext.startsWith('//')
|
|
81
|
-
) {
|
|
82
|
-
try {
|
|
83
|
-
const parsed = new URL(requestedNext, window.location.origin);
|
|
84
|
-
// Catches WHATWG bypasses like `/\evil.com` that resolve to a
|
|
85
|
-
// different origin even though the raw input started with `/`.
|
|
86
|
-
if (parsed.origin === window.location.origin) {
|
|
87
|
-
return parsed.pathname + parsed.search + parsed.hash;
|
|
88
|
-
}
|
|
89
|
-
} catch {
|
|
90
|
-
// fall through to default redirect
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return '/';
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
useEffect(() => {
|
|
98
|
-
const socialError = params.get('error') || params.get('social');
|
|
99
|
-
if (socialError) {
|
|
100
|
-
setErrorKey(t('Auth.SOCIAL_LOGIN_FAILED'));
|
|
101
|
-
}
|
|
102
|
-
}, [location.search]);
|
|
103
|
-
|
|
104
|
-
// S164 handoff: on `#recovery=ok`, pull the plaintext token out of the
|
|
105
|
-
// server-side session (one-shot). On `invalid`/`expired`, or on any
|
|
106
|
-
// other non-empty value (legacy pre-S164 email links that carried the
|
|
107
|
-
// token directly in the hash), surface an invalid-link error so the
|
|
108
|
-
// user gets feedback instead of a silent login screen.
|
|
109
|
-
useEffect(() => {
|
|
110
|
-
if (!recoveryStatus) {
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
// Strip the `recovery=…` sentinel from BOTH hash and query string so a
|
|
114
|
-
// bookmark or page refresh does not re-trigger this effect against an
|
|
115
|
-
// already-popped session entry (which would render a confusing
|
|
116
|
-
// "invalid" error to a user who is mid-login).
|
|
117
|
-
//
|
|
118
|
-
// We use `history.replaceState` rather than `navigate(..., {replace})`
|
|
119
|
-
// intentionally: react-router's `navigate` would trigger an immediate
|
|
120
|
-
// re-render with an updated `useLocation()`, which would cause this
|
|
121
|
-
// effect's cleanup to set `cancelled=true` and discard the in-flight
|
|
122
|
-
// recovery-token fetch result. `replaceState` only mutates the browser
|
|
123
|
-
// URL bar; `useLocation()` stays at `recovery=ok` for this render, the
|
|
124
|
-
// fetch resolves, and `recoveryToken` gets set normally. The trade-off
|
|
125
|
-
// is that `useLocation().hash` is stale for the page lifetime — fine
|
|
126
|
-
// because nothing else in this component reads it.
|
|
127
|
-
if (typeof window !== 'undefined' && window.history?.replaceState) {
|
|
128
|
-
const cleanParams = new URLSearchParams(window.location.search);
|
|
129
|
-
cleanParams.delete('recovery');
|
|
130
|
-
const cleanSearch = cleanParams.toString();
|
|
131
|
-
const cleanUrl =
|
|
132
|
-
window.location.pathname + (cleanSearch ? `?${cleanSearch}` : '');
|
|
133
|
-
window.history.replaceState(null, '', cleanUrl);
|
|
134
|
-
}
|
|
135
|
-
if (recoveryStatus === 'expired') {
|
|
136
|
-
setErrorKey(t('Auth.RECOVERY_TOKEN_EXPIRED'));
|
|
137
|
-
setRecoveryToken(null);
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
if (recoveryStatus !== 'ok') {
|
|
141
|
-
// Catches `invalid` and any unknown value (including legacy email
|
|
142
|
-
// links that carried the plaintext token before S164).
|
|
143
|
-
setErrorKey(t('Auth.RECOVERY_TOKEN_INVALID'));
|
|
144
|
-
setRecoveryToken(null);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
let cancelled = false;
|
|
148
|
-
setRecoveryFetching(true);
|
|
149
|
-
fetchRecoverySessionToken()
|
|
150
|
-
.then((token) => {
|
|
151
|
-
if (cancelled) return;
|
|
152
|
-
if (!token) {
|
|
153
|
-
// Session entry missing or already consumed — treat as invalid.
|
|
154
|
-
setErrorKey(t('Auth.RECOVERY_TOKEN_INVALID'));
|
|
155
|
-
setRecoveryToken(null);
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
setRecoveryToken(token);
|
|
159
|
-
})
|
|
160
|
-
.catch((err) => {
|
|
161
|
-
if (cancelled) return;
|
|
162
|
-
setErrorKey(resolveErrorText(i18n, err, 'Auth.RECOVERY_TOKEN_INVALID'));
|
|
163
|
-
setRecoveryToken(null);
|
|
164
|
-
})
|
|
165
|
-
.finally(() => {
|
|
166
|
-
if (!cancelled) {
|
|
167
|
-
setRecoveryFetching(false);
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
return () => {
|
|
171
|
-
cancelled = true;
|
|
172
|
-
};
|
|
173
|
-
}, [recoveryStatus]);
|
|
174
|
-
|
|
175
|
-
useEffect(() => {
|
|
176
|
-
if (loading || !user) return;
|
|
177
|
-
navigate(getRedirectTarget(user), { replace: true });
|
|
178
|
-
}, [loading, user, navigate, requestedNext]);
|
|
179
|
-
|
|
180
|
-
// --- Helper: Central Success Logic ---
|
|
181
|
-
const handleLoginSuccess = (user) => {
|
|
182
|
-
login(user); // Update Context
|
|
183
|
-
navigate(getRedirectTarget(user));
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
// --- Handlers ---
|
|
187
|
-
|
|
188
|
-
const handleSubmitCredentials = async ({ identifier, password }) => {
|
|
189
|
-
setErrorKey(null);
|
|
190
|
-
setSubmitting(true);
|
|
191
|
-
try {
|
|
192
|
-
// A) Recovery Flow
|
|
193
|
-
if (recoveryToken) {
|
|
194
|
-
const result = await loginWithRecoveryPassword(
|
|
195
|
-
identifier,
|
|
196
|
-
password,
|
|
197
|
-
recoveryToken
|
|
198
|
-
);
|
|
199
|
-
login(result.user);
|
|
200
|
-
navigate(
|
|
201
|
-
getRedirectTarget(result.user, {
|
|
202
|
-
forceSecurityRedirect: '/account?tab=security&from=recovery',
|
|
203
|
-
}),
|
|
204
|
-
);
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// B) Standard Password Login
|
|
209
|
-
const result = await loginWithPassword(identifier, password);
|
|
210
|
-
|
|
211
|
-
if (result.needsMfa) {
|
|
212
|
-
setMfaState({
|
|
213
|
-
availableTypes: result.availableTypes || [],
|
|
214
|
-
identifier,
|
|
215
|
-
});
|
|
216
|
-
setStep('mfa');
|
|
217
|
-
} else {
|
|
218
|
-
handleLoginSuccess(result.user);
|
|
219
|
-
}
|
|
220
|
-
} catch (err) {
|
|
221
|
-
setErrorKey(resolveErrorText(i18n, err, 'Auth.LOGIN_FAILED'));
|
|
222
|
-
} finally {
|
|
223
|
-
setSubmitting(false);
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
const handlePasskeyLoginInitial = async () => {
|
|
228
|
-
setErrorKey(null);
|
|
229
|
-
setSubmitting(true);
|
|
230
|
-
try {
|
|
231
|
-
// Service handles browser interaction + API calls
|
|
232
|
-
const user = await loginWithPasskey();
|
|
233
|
-
handleLoginSuccess(user);
|
|
234
|
-
} catch (err) {
|
|
235
|
-
// 'Auth.PASSKEY_CANCELLED' is generic, maybe ignore visually or show specific hint
|
|
236
|
-
if (err.code !== 'Auth.PASSKEY_CANCELLED') {
|
|
237
|
-
setErrorKey(resolveErrorText(i18n, err, 'Auth.PASSKEY_FAILED'));
|
|
238
|
-
}
|
|
239
|
-
} finally {
|
|
240
|
-
setSubmitting(false);
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
const handleMfaSuccess = ({ user, method }) => {
|
|
245
|
-
// MFA component should return the user object after verifying code
|
|
246
|
-
if (method === 'recovery_code') {
|
|
247
|
-
login(user);
|
|
248
|
-
navigate(
|
|
249
|
-
getRedirectTarget(user, {
|
|
250
|
-
forceSecurityRedirect: '/account?tab=security&from=recovery',
|
|
251
|
-
}),
|
|
252
|
-
);
|
|
253
|
-
} else {
|
|
254
|
-
handleLoginSuccess(user);
|
|
255
|
-
}
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
const handleMfaCancel = () => {
|
|
259
|
-
setStep('credentials');
|
|
260
|
-
setMfaState(null);
|
|
261
|
-
setErrorKey(null);
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
const socialProviders = Array.isArray(authMethods?.social_providers)
|
|
265
|
-
? authMethods.social_providers
|
|
266
|
-
: [];
|
|
267
|
-
const passwordLoginEnabled = Boolean(authMethods?.password_login) || Boolean(recoveryToken);
|
|
268
|
-
const socialLoginEnabled = Boolean(authMethods?.social_login) && socialProviders.length > 0;
|
|
269
|
-
const passkeyLoginEnabled = Boolean(authMethods?.passkey_login);
|
|
270
|
-
const signupModes = Array.isArray(authMethods?.signup_modes)
|
|
271
|
-
? authMethods.signup_modes.filter(Boolean)
|
|
272
|
-
: [];
|
|
273
|
-
const signupEnabled = signupModes.length > 0 || Boolean(authMethods?.signup);
|
|
274
|
-
const passwordResetEnabled = Boolean(authMethods?.password_reset);
|
|
275
|
-
const twoFactorRequired = Boolean(authMethods?.two_factor_required)
|
|
276
|
-
|| Number(authMethods?.required_auth_factor_count || 1) >= 2;
|
|
277
|
-
|
|
278
|
-
// --- Render ---
|
|
279
|
-
|
|
280
|
-
return (
|
|
281
|
-
<NarrowPage
|
|
282
|
-
title={t('Auth.PAGE_LOGIN_TITLE')}
|
|
283
|
-
subtitle={t('Auth.PAGE_LOGIN_SUBTITLE')}
|
|
284
|
-
>
|
|
285
|
-
<Helmet>
|
|
286
|
-
<title>{t('App.NAME')} – {t('Auth.PAGE_LOGIN_TITLE')}</title>
|
|
287
|
-
</Helmet>
|
|
288
|
-
|
|
289
|
-
{errorKey && (
|
|
290
|
-
<Alert severity="error" sx={LOGIN_PAGE_ALERT_SX}>
|
|
291
|
-
{errorKey}
|
|
292
|
-
</Alert>
|
|
293
|
-
)}
|
|
294
|
-
|
|
295
|
-
{recoveryFetching && !errorKey && (
|
|
296
|
-
<Alert severity="info" sx={LOGIN_PAGE_ALERT_SX}>
|
|
297
|
-
{t('Auth.RECOVERY_LINK_VALIDATING', 'Validating recovery link…')}
|
|
298
|
-
</Alert>
|
|
299
|
-
)}
|
|
300
|
-
|
|
301
|
-
{recoveryToken && !errorKey && (
|
|
302
|
-
<Alert severity="info" sx={LOGIN_PAGE_ALERT_SX}>
|
|
303
|
-
{t('Auth.RECOVERY_LOGIN_WARNING', 'Recovery link validated. Please enter your password.')}
|
|
304
|
-
</Alert>
|
|
305
|
-
)}
|
|
306
|
-
|
|
307
|
-
{twoFactorRequired && !recoveryToken && !recoveryFetching && (
|
|
308
|
-
<Alert severity="info" sx={LOGIN_PAGE_ALERT_SX}>
|
|
309
|
-
{t('Auth.TWO_FACTOR_REQUIRED_HINT', 'This app requires two authentication factors for full access.')}
|
|
310
|
-
</Alert>
|
|
311
|
-
)}
|
|
312
|
-
|
|
313
|
-
{step === 'credentials' && (
|
|
314
|
-
<LoginForm
|
|
315
|
-
onSubmit={passwordLoginEnabled ? handleSubmitCredentials : null}
|
|
316
|
-
onForgotPassword={
|
|
317
|
-
passwordResetEnabled ? () => navigate('/reset-request-password') : null
|
|
318
|
-
}
|
|
319
|
-
onSocialLogin={socialLoginEnabled ? (provider) => startSocialLogin(provider) : null}
|
|
320
|
-
socialProviders={socialProviders}
|
|
321
|
-
onPasskeyLogin={passkeyLoginEnabled ? handlePasskeyLoginInitial : null}
|
|
322
|
-
onSignUp={signupEnabled ? () => navigate('/signup') : null}
|
|
323
|
-
// Block submit while the handoff is in flight so the user does not
|
|
324
|
-
// race the recovery-token fetch with a normal password POST.
|
|
325
|
-
disabled={submitting || recoveryFetching}
|
|
326
|
-
initialIdentifier={recoveryEmail}
|
|
327
|
-
/>
|
|
328
|
-
)}
|
|
329
|
-
|
|
330
|
-
{step === 'mfa' && mfaState && (
|
|
331
|
-
<Box>
|
|
332
|
-
{/* Assuming MfaLoginComponent handles the API call to authenticateWithMFA */}
|
|
333
|
-
<MfaLoginComponent
|
|
334
|
-
availableTypes={mfaState.availableTypes}
|
|
335
|
-
identifier={mfaState.identifier}
|
|
336
|
-
onSuccess={handleMfaSuccess}
|
|
337
|
-
onCancel={handleMfaCancel}
|
|
338
|
-
/>
|
|
339
|
-
</Box>
|
|
340
|
-
)}
|
|
341
|
-
</NarrowPage>
|
|
342
|
-
);
|
|
343
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
// src/auth/pages/PasswordChangePage.jsx
|
|
2
|
-
import React, { useState } from 'react';
|
|
3
|
-
import { Helmet } from 'react-helmet';
|
|
4
|
-
import { Typography } from '@mui/material';
|
|
5
|
-
import { useTranslation } from 'react-i18next';
|
|
6
|
-
import { NarrowPage } from '../layout/PageLayout';
|
|
7
|
-
import { PasswordChangeForm } from '../components/PasswordChangeForm';
|
|
8
|
-
import { changePassword } from '../auth/authApi';
|
|
9
|
-
import { resolveErrorText } from '../utils/resolveErrorText';
|
|
10
|
-
|
|
11
|
-
export function PasswordChangePage() {
|
|
12
|
-
const { t, i18n } = useTranslation();
|
|
13
|
-
|
|
14
|
-
const [submitting, setSubmitting] = useState(false);
|
|
15
|
-
const [successKey, setSuccessKey] = useState(null);
|
|
16
|
-
const [errorKey, setErrorKey] = useState(null);
|
|
17
|
-
|
|
18
|
-
const handleSubmit = async (oldPassword, newPassword) => {
|
|
19
|
-
setSuccessKey(null);
|
|
20
|
-
setErrorKey(null);
|
|
21
|
-
setSubmitting(true);
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
await changePassword(oldPassword, newPassword);
|
|
25
|
-
setSuccessKey('Auth.RESET_PASSWORD_SUCCESS');
|
|
26
|
-
} catch (err) {
|
|
27
|
-
setErrorKey(resolveErrorText(i18n, err, 'Auth.PASSWORD_CHANGE_FAILED'));
|
|
28
|
-
} finally {
|
|
29
|
-
setSubmitting(false);
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<NarrowPage
|
|
35
|
-
title={t('Auth.CHANGE_PASSWORD_BUTTON')}
|
|
36
|
-
subtitle={t('Auth.PAGE_CHANGE_PASSWORD_SUBTITLE')}
|
|
37
|
-
>
|
|
38
|
-
<Helmet>
|
|
39
|
-
<title>
|
|
40
|
-
{t('App.NAME')} – {t('Auth.CHANGE_PASSWORD_BUTTON')}
|
|
41
|
-
</title>
|
|
42
|
-
</Helmet>
|
|
43
|
-
|
|
44
|
-
{successKey && (
|
|
45
|
-
<Typography color="primary" gutterBottom>
|
|
46
|
-
{t(successKey)}
|
|
47
|
-
</Typography>
|
|
48
|
-
)}
|
|
49
|
-
|
|
50
|
-
{errorKey && (
|
|
51
|
-
<Typography color="error" gutterBottom>
|
|
52
|
-
{errorKey}
|
|
53
|
-
</Typography>
|
|
54
|
-
)}
|
|
55
|
-
|
|
56
|
-
<PasswordChangeForm
|
|
57
|
-
onSubmit={handleSubmit}
|
|
58
|
-
submitting={submitting}
|
|
59
|
-
/>
|
|
60
|
-
</NarrowPage>
|
|
61
|
-
);
|
|
62
|
-
}
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
// src/auth/pages/PasswordInvitePage.jsx
|
|
2
|
-
import React, { useState, useEffect } from 'react';
|
|
3
|
-
import { useNavigate, useParams, useLocation } from 'react-router-dom';
|
|
4
|
-
import { Helmet } from 'react-helmet';
|
|
5
|
-
import { Button, Typography } from '@mui/material';
|
|
6
|
-
import { useTranslation } from 'react-i18next';
|
|
7
|
-
import { NarrowPage } from '../layout/PageLayout';
|
|
8
|
-
import { PasswordSetForm } from '../components/PasswordSetForm';
|
|
9
|
-
import { verifyResetToken, setNewPassword } from '../auth/authApi';
|
|
10
|
-
import { resolveErrorText } from '../utils/resolveErrorText';
|
|
11
|
-
|
|
12
|
-
export function PasswordInvitePage() {
|
|
13
|
-
const { uid, token } = useParams();
|
|
14
|
-
const location = useLocation();
|
|
15
|
-
const navigate = useNavigate();
|
|
16
|
-
const { t, i18n } = useTranslation();
|
|
17
|
-
const searchParams = new URLSearchParams(location.search);
|
|
18
|
-
const nextPath = searchParams.get('next');
|
|
19
|
-
|
|
20
|
-
const [submitting, setSubmitting] = useState(false);
|
|
21
|
-
const [errorKey, setErrorKey] = useState(null);
|
|
22
|
-
const [submitErrorKey, setSubmitErrorKey] = useState(null);
|
|
23
|
-
const [successKey, setSuccessKey] = useState(null);
|
|
24
|
-
const [continueTarget, setContinueTarget] = useState(null);
|
|
25
|
-
const [checked, setChecked] = useState(false);
|
|
26
|
-
|
|
27
|
-
// Unterscheidung Invite-Link vs klassischer Reset
|
|
28
|
-
const isInvite = location.pathname.startsWith('/invite/');
|
|
29
|
-
|
|
30
|
-
// Page-Titel/Subtitel-Keys anhand isInvite bestimmen
|
|
31
|
-
const pageTitleKey = isInvite
|
|
32
|
-
? 'Auth.PAGE_INVITE_TITLE'
|
|
33
|
-
: 'Auth.PAGE_RESET_PASSWORD_TITLE';
|
|
34
|
-
|
|
35
|
-
const pageSubtitleKey = isInvite
|
|
36
|
-
? 'Auth.PAGE_INVITE_SUBTITLE'
|
|
37
|
-
: 'Auth.PAGE_RESET_PASSWORD_SUBTITLE';
|
|
38
|
-
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
if (!uid || !token) {
|
|
41
|
-
setErrorKey(t('Auth.RESET_LINK_INVALID'));
|
|
42
|
-
setChecked(true);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const check = async () => {
|
|
47
|
-
try {
|
|
48
|
-
await verifyResetToken(uid, token);
|
|
49
|
-
setChecked(true);
|
|
50
|
-
} catch (err) {
|
|
51
|
-
setErrorKey(resolveErrorText(i18n, err, 'Auth.RESET_LINK_INVALID'));
|
|
52
|
-
setChecked(true);
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
check();
|
|
57
|
-
}, [uid, token]);
|
|
58
|
-
|
|
59
|
-
const handleSubmit = async (newPassword) => {
|
|
60
|
-
if (!uid || !token) {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
setSubmitting(true);
|
|
65
|
-
setSubmitErrorKey(null);
|
|
66
|
-
setSuccessKey(null);
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
await setNewPassword(uid, token, newPassword);
|
|
70
|
-
setSuccessKey(
|
|
71
|
-
isInvite
|
|
72
|
-
? 'Auth.RESET_PASSWORD_SUCCESS_INVITE'
|
|
73
|
-
: 'Auth.RESET_PASSWORD_SUCCESS_RESET',
|
|
74
|
-
);
|
|
75
|
-
// Same-origin check via URL parser — startsWith('/') alone misses
|
|
76
|
-
// bypasses like `/\evil.com`, which WHATWG-parses to `https://evil.com/`
|
|
77
|
-
// for special schemes. The parser normalises backslashes and protocol-
|
|
78
|
-
// relative forms, so any cross-origin result is caught here.
|
|
79
|
-
let safeNextPath = null;
|
|
80
|
-
// Require leading-slash absolute path on the raw input (rejects
|
|
81
|
-
// relative inputs like `account/settings` and protocol-relative `//host`
|
|
82
|
-
// before they reach the URL parser, which would normalise both into a
|
|
83
|
-
// same-origin pathname starting with `/`).
|
|
84
|
-
if (
|
|
85
|
-
nextPath &&
|
|
86
|
-
typeof nextPath === 'string' &&
|
|
87
|
-
nextPath.startsWith('/') &&
|
|
88
|
-
!nextPath.startsWith('//')
|
|
89
|
-
) {
|
|
90
|
-
try {
|
|
91
|
-
const parsed = new URL(nextPath, window.location.origin);
|
|
92
|
-
// Catches WHATWG bypasses like `/\evil.com` that resolve to a
|
|
93
|
-
// different origin even though the raw input started with `/`.
|
|
94
|
-
if (parsed.origin === window.location.origin) {
|
|
95
|
-
safeNextPath = parsed.pathname + parsed.search + parsed.hash;
|
|
96
|
-
}
|
|
97
|
-
} catch {
|
|
98
|
-
// fall through to default redirect
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
const target = safeNextPath
|
|
102
|
-
? `/login?next=${encodeURIComponent(safeNextPath)}`
|
|
103
|
-
: '/login';
|
|
104
|
-
setContinueTarget(target);
|
|
105
|
-
} catch (err) {
|
|
106
|
-
// Told apart by CODE, not by the presence of `messages` (a submission
|
|
107
|
-
// can fail with a link problem too, e.g. the token expires in the gap
|
|
108
|
-
// between the page's initial verifyResetToken check and this submit) —
|
|
109
|
-
// only a password rejection keeps the form; a link problem must still
|
|
110
|
-
// end in the same invalid-link state the initial check produces.
|
|
111
|
-
if (err.code === 'Auth.RESET_LINK_INVALID') {
|
|
112
|
-
setErrorKey(resolveErrorText(i18n, err, 'Auth.RESET_LINK_INVALID'));
|
|
113
|
-
} else {
|
|
114
|
-
const responseMessages = err.raw?.messages || err.response?.data?.messages;
|
|
115
|
-
setSubmitErrorKey(
|
|
116
|
-
Array.isArray(responseMessages) && responseMessages.length > 0
|
|
117
|
-
? responseMessages.join(' ')
|
|
118
|
-
: resolveErrorText(i18n, err, 'Auth.RESET_PASSWORD_FAILED'),
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
} finally {
|
|
122
|
-
setSubmitting(false);
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
// Loading/Validierung des Links
|
|
127
|
-
if (!checked && !errorKey) {
|
|
128
|
-
return (
|
|
129
|
-
<NarrowPage title={t('Auth.PAGE_CHECKING_LINK_TITLE')}>
|
|
130
|
-
<Typography>{t('Auth.PAGE_CHECKING_LINK_TEXT')}</Typography>
|
|
131
|
-
</NarrowPage>
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return (
|
|
136
|
-
<NarrowPage
|
|
137
|
-
title={t(pageTitleKey)}
|
|
138
|
-
subtitle={t(pageSubtitleKey)}
|
|
139
|
-
>
|
|
140
|
-
<Helmet>
|
|
141
|
-
<title>
|
|
142
|
-
{t('App.NAME')} – {t(pageTitleKey)}
|
|
143
|
-
</title>
|
|
144
|
-
</Helmet>
|
|
145
|
-
|
|
146
|
-
{errorKey && (
|
|
147
|
-
<Typography color="error" gutterBottom>
|
|
148
|
-
{errorKey}
|
|
149
|
-
</Typography>
|
|
150
|
-
)}
|
|
151
|
-
|
|
152
|
-
{successKey && (
|
|
153
|
-
<>
|
|
154
|
-
<Typography color="primary" gutterBottom>
|
|
155
|
-
{t(successKey)}
|
|
156
|
-
</Typography>
|
|
157
|
-
<Button
|
|
158
|
-
variant="contained"
|
|
159
|
-
onClick={() => navigate(continueTarget)}
|
|
160
|
-
>
|
|
161
|
-
{t('Auth.SIGNUP_GO_TO_LOGIN')}
|
|
162
|
-
</Button>
|
|
163
|
-
</>
|
|
164
|
-
)}
|
|
165
|
-
|
|
166
|
-
{submitErrorKey && !successKey && (
|
|
167
|
-
<Typography color="error" gutterBottom>
|
|
168
|
-
{submitErrorKey}
|
|
169
|
-
</Typography>
|
|
170
|
-
)}
|
|
171
|
-
|
|
172
|
-
{!successKey && !errorKey && (
|
|
173
|
-
<PasswordSetForm onSubmit={handleSubmit} submitting={submitting} />
|
|
174
|
-
)}
|
|
175
|
-
</NarrowPage>
|
|
176
|
-
);
|
|
177
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
// src/auth/pages/PasswordResetRequestPage.jsx
|
|
2
|
-
import React, { useState } from 'react';
|
|
3
|
-
import { Helmet } from 'react-helmet';
|
|
4
|
-
import { Typography } from '@mui/material';
|
|
5
|
-
import { useTranslation } from 'react-i18next';
|
|
6
|
-
import { NarrowPage } from '../layout/PageLayout';
|
|
7
|
-
import { requestPasswordReset } from '../auth/authApi';
|
|
8
|
-
import {PasswordResetRequestForm } from '../components/PasswordResetRequestForm';
|
|
9
|
-
import { resolveErrorText } from '../utils/resolveErrorText';
|
|
10
|
-
|
|
11
|
-
export function PasswordResetRequestPage() {
|
|
12
|
-
const { t, i18n } = useTranslation();
|
|
13
|
-
|
|
14
|
-
const [submitting, setSubmitting] = useState(false);
|
|
15
|
-
const [successKey, setSuccessKey] = useState(null);
|
|
16
|
-
const [errorKey, setErrorKey] = useState(null);
|
|
17
|
-
|
|
18
|
-
const handleSubmit = async (email) => {
|
|
19
|
-
setSuccessKey(null);
|
|
20
|
-
setErrorKey(null);
|
|
21
|
-
|
|
22
|
-
if (!email) {
|
|
23
|
-
setErrorKey(t('Auth.EMAIL_REQUIRED'));
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
setSubmitting(true);
|
|
28
|
-
try {
|
|
29
|
-
await requestPasswordReset(email);
|
|
30
|
-
// Kein User-Leak, immer gleiche Success-Meldung
|
|
31
|
-
setSuccessKey('Auth.RESET_REQUEST_ACCEPTED');
|
|
32
|
-
} catch (err) {
|
|
33
|
-
setErrorKey(resolveErrorText(i18n, err, 'Auth.RESET_REQUEST_FAILED'));
|
|
34
|
-
} finally {
|
|
35
|
-
setSubmitting(false);
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<NarrowPage
|
|
41
|
-
title={t('Auth.PAGE_RESET_PASSWORD_TITLE')}
|
|
42
|
-
subtitle={t('Auth.PAGE_RESET_REQUEST_SUBTITLE')}
|
|
43
|
-
>
|
|
44
|
-
<Helmet>
|
|
45
|
-
<title>
|
|
46
|
-
{t('App.NAME')} – {t('Auth.PAGE_RESET_PASSWORD_TITLE')}
|
|
47
|
-
</title>
|
|
48
|
-
</Helmet>
|
|
49
|
-
|
|
50
|
-
{successKey && (
|
|
51
|
-
<Typography color="primary" gutterBottom>
|
|
52
|
-
{t(successKey)}
|
|
53
|
-
</Typography>
|
|
54
|
-
)}
|
|
55
|
-
|
|
56
|
-
{errorKey && (
|
|
57
|
-
<Typography color="error" gutterBottom>
|
|
58
|
-
{errorKey}
|
|
59
|
-
</Typography>
|
|
60
|
-
)}
|
|
61
|
-
|
|
62
|
-
<PasswordResetRequestForm
|
|
63
|
-
onSubmit={handleSubmit}
|
|
64
|
-
submitting={submitting}
|
|
65
|
-
/>
|
|
66
|
-
</NarrowPage>
|
|
67
|
-
);
|
|
68
|
-
}
|