@micha.bigler/ui-core-micha 3.14.4 → 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 +25 -0
- package/README.md +7 -0
- package/dist/chat/kitSxRegistry.js +9 -0
- 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/pages/AccountPage.js +1 -2
- package/dist/pages/LoginPage.js +1 -2
- package/dist/pages/PasswordChangePage.js +1 -2
- package/dist/pages/PasswordInvitePage.js +1 -2
- package/dist/pages/PasswordResetRequestPage.js +1 -2
- package/dist/pages/SignUpPage.js +1 -2
- package/dist/pages/SignupConfirmPage.js +1 -2
- package/dist/theme/kitSxEntry.js +11 -0
- package/dist/theme/kitSxRegistry.js +2 -15
- package/package.json +32 -7
- package/.github/workflows/ci.yml +0 -35
- package/.github/workflows/publish.yml +0 -126
- package/DESIGN.md +0 -60
- package/WORK_ORDERS.md +0 -150
- 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-CI-1.md +0 -65
- 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/utils/authService.js
DELETED
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getPasskeyRegistrationOptions,
|
|
3
|
-
completePasskeyRegistration,
|
|
4
|
-
getPasskeyLoginOptions,
|
|
5
|
-
completePasskeyLogin,
|
|
6
|
-
fetchCurrentUser,
|
|
7
|
-
authenticateWithMFA
|
|
8
|
-
} from '../auth/authApi';
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
ensureWebAuthnSupport,
|
|
12
|
-
serializeCredential
|
|
13
|
-
} from '../utils/webauthn';
|
|
14
|
-
import { normaliseApiError } from '../utils/auth-errors';
|
|
15
|
-
import apiClient from '../auth/apiClient';
|
|
16
|
-
import { HEADLESS_BASE } from '../auth/authConfig';
|
|
17
|
-
|
|
18
|
-
// HILFSFUNKTION: Entpackt die Optionen, falls sie in 'publicKey' stecken
|
|
19
|
-
function resolveWebAuthnOptions(options) {
|
|
20
|
-
if (options && options.publicKey) {
|
|
21
|
-
return options.publicKey;
|
|
22
|
-
}
|
|
23
|
-
return options;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function registerPasskey(name = 'Passkey') {
|
|
27
|
-
ensureWebAuthnSupport();
|
|
28
|
-
|
|
29
|
-
// 1. Options vom Server
|
|
30
|
-
const optionsEnvelope = await getPasskeyRegistrationOptions();
|
|
31
|
-
// optionsEnvelope ist bei dir: { publicKey: { ... } } oder { creation_options: { publicKey: {...} } }
|
|
32
|
-
|
|
33
|
-
// Extrahiere die eigentlichen publicKey-Options
|
|
34
|
-
const publicKeyJson =
|
|
35
|
-
(optionsEnvelope.creation_options && optionsEnvelope.creation_options.publicKey) ||
|
|
36
|
-
optionsEnvelope.publicKey ||
|
|
37
|
-
optionsEnvelope;
|
|
38
|
-
|
|
39
|
-
let credential;
|
|
40
|
-
try {
|
|
41
|
-
const publicKey = window.PublicKeyCredential.parseCreationOptionsFromJSON(
|
|
42
|
-
publicKeyJson,
|
|
43
|
-
);
|
|
44
|
-
// Wichtig: create({ publicKey: ... })
|
|
45
|
-
credential = await navigator.credentials.create({ publicKey });
|
|
46
|
-
} catch (err) {
|
|
47
|
-
if (err.name === 'NotAllowedError') {
|
|
48
|
-
throw normaliseApiError(
|
|
49
|
-
new Error('Auth.PASSKEY_CANCELLED'),
|
|
50
|
-
'Auth.PASSKEY_CANCELLED',
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
throw normaliseApiError(err, 'Auth.PASSKEY_CREATE_FAILED');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const credentialJson = serializeCredential(credential);
|
|
57
|
-
return completePasskeyRegistration(credentialJson, name);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export async function loginWithPasskey() {
|
|
61
|
-
ensureWebAuthnSupport();
|
|
62
|
-
|
|
63
|
-
// 1. Get Challenge
|
|
64
|
-
const requestOptions = await getPasskeyLoginOptions();
|
|
65
|
-
|
|
66
|
-
// 2. Browser Sign
|
|
67
|
-
let assertion;
|
|
68
|
-
try {
|
|
69
|
-
// FIX: Erst entpacken
|
|
70
|
-
const optionsJson = resolveWebAuthnOptions(requestOptions);
|
|
71
|
-
|
|
72
|
-
// Dann parsen
|
|
73
|
-
const publicKeyOptions =
|
|
74
|
-
window.PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson);
|
|
75
|
-
assertion = await navigator.credentials.get({ publicKey: publicKeyOptions });
|
|
76
|
-
} catch (err) {
|
|
77
|
-
if (err.name === 'NotAllowedError') {
|
|
78
|
-
throw normaliseApiError(new Error('Auth.PASSKEY_CANCELLED'), 'Auth.PASSKEY_CANCELLED');
|
|
79
|
-
}
|
|
80
|
-
throw err;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// 3. Complete
|
|
84
|
-
const credentialJson = serializeCredential(assertion);
|
|
85
|
-
await completePasskeyLogin(credentialJson);
|
|
86
|
-
|
|
87
|
-
// 4. Reload User
|
|
88
|
-
return fetchCurrentUser();
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* WebAuthn als 2. Faktor nutzen (wenn Passwort schon eingegeben wurde).
|
|
93
|
-
*/
|
|
94
|
-
export async function authenticateMfaWithPasskey() {
|
|
95
|
-
ensureWebAuthnSupport();
|
|
96
|
-
|
|
97
|
-
let requestOptions;
|
|
98
|
-
try {
|
|
99
|
-
const url = typeof HEADLESS_BASE !== 'undefined'
|
|
100
|
-
? `${HEADLESS_BASE}/auth/2fa/authenticate`
|
|
101
|
-
: '/api/auth/browser/v1/auth/2fa/authenticate';
|
|
102
|
-
|
|
103
|
-
const res = await apiClient.get(url);
|
|
104
|
-
// Allauth liefert oft: { data: { request_options: { publicKey: ... } } }
|
|
105
|
-
const data = res.data?.data || res.data;
|
|
106
|
-
requestOptions = data.request_options || data;
|
|
107
|
-
} catch (err) {
|
|
108
|
-
throw normaliseApiError(err, 'Auth.MFA_CHALLENGE_FAILED');
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (!requestOptions) {
|
|
112
|
-
throw new Error('No WebAuthn challenge received for MFA.');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// 2. Browser Sign
|
|
116
|
-
let assertion;
|
|
117
|
-
try {
|
|
118
|
-
// FIX: Erst entpacken
|
|
119
|
-
const optionsJson = resolveWebAuthnOptions(requestOptions);
|
|
120
|
-
|
|
121
|
-
// Dann parsen
|
|
122
|
-
const publicKeyOptions = window.PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson);
|
|
123
|
-
|
|
124
|
-
// Dann wrappen
|
|
125
|
-
assertion = await navigator.credentials.get({ publicKey: publicKeyOptions });
|
|
126
|
-
} catch (err) {
|
|
127
|
-
if (err.name === 'NotAllowedError') {
|
|
128
|
-
throw normaliseApiError(new Error('Auth.PASSKEY_CANCELLED'), 'Auth.PASSKEY_CANCELLED');
|
|
129
|
-
}
|
|
130
|
-
throw err;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// 3. Authenticate via existing API function
|
|
134
|
-
const credentialJson = serializeCredential(assertion);
|
|
135
|
-
return authenticateWithMFA({ credential: credentialJson });
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function getCsrfTokenFromCookie() {
|
|
139
|
-
if (typeof document === 'undefined' || !document.cookie) return null;
|
|
140
|
-
const match = document.cookie.match(/(?:^|; )csrftoken=([^;]+)/);
|
|
141
|
-
return match ? decodeURIComponent(match[1]) : null;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function submitSocialRedirectForm({ provider, callbackUrl, csrfToken, process }) {
|
|
145
|
-
const form = document.createElement('form');
|
|
146
|
-
form.method = 'POST';
|
|
147
|
-
form.action = `${HEADLESS_BASE}/auth/provider/redirect`;
|
|
148
|
-
form.style.display = 'none';
|
|
149
|
-
|
|
150
|
-
const fields = {
|
|
151
|
-
provider,
|
|
152
|
-
process,
|
|
153
|
-
callback_url: callbackUrl,
|
|
154
|
-
csrfmiddlewaretoken: csrfToken,
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
Object.entries(fields).forEach(([name, value]) => {
|
|
158
|
-
const input = document.createElement('input');
|
|
159
|
-
input.type = 'hidden';
|
|
160
|
-
input.name = name;
|
|
161
|
-
input.value = String(value);
|
|
162
|
-
form.appendChild(input);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
document.body.appendChild(form);
|
|
166
|
-
form.submit();
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export async function startSocialLogin(provider, options = {}) {
|
|
170
|
-
if (typeof window === 'undefined') {
|
|
171
|
-
throw normaliseApiError(
|
|
172
|
-
new Error('Auth.SOCIAL_LOGIN_NOT_IN_BROWSER'),
|
|
173
|
-
'Auth.SOCIAL_LOGIN_NOT_IN_BROWSER'
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
try {
|
|
178
|
-
// Ensures csrftoken cookie exists before form POST.
|
|
179
|
-
await apiClient.get('/api/csrf/');
|
|
180
|
-
} catch {
|
|
181
|
-
// Continue; token might already be present.
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const csrfToken = getCsrfTokenFromCookie();
|
|
185
|
-
if (!csrfToken) {
|
|
186
|
-
throw normaliseApiError(
|
|
187
|
-
new Error('Auth.SOCIAL_LOGIN_FAILED'),
|
|
188
|
-
'Auth.SOCIAL_LOGIN_FAILED',
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const process = options.process === 'connect' ? 'connect' : 'login';
|
|
193
|
-
const originUrl = new URL(window.location.origin);
|
|
194
|
-
if (originUrl.hostname.startsWith('www.')) {
|
|
195
|
-
originUrl.hostname = originUrl.hostname.slice(4);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// S62: Same-Origin-Validation für `callbackUrl`. Verhindert, dass künftige
|
|
199
|
-
// Konsumenten mit tainted Wert (z.B. `?next=https://evil.example/`) einen
|
|
200
|
-
// Open-Redirect via OAuth-Callback-Pfad erzeugen. Caller heute safe
|
|
201
|
-
// (`options.callbackUrl` wird nur intern gesetzt), aber defensive depth.
|
|
202
|
-
let callbackUrl = `${originUrl.origin}/login`;
|
|
203
|
-
if (options.callbackUrl) {
|
|
204
|
-
try {
|
|
205
|
-
const candidate = new URL(options.callbackUrl, window.location.origin);
|
|
206
|
-
if (candidate.origin === originUrl.origin) {
|
|
207
|
-
callbackUrl = candidate.toString();
|
|
208
|
-
} else {
|
|
209
|
-
// Cross-origin → silent fallback auf Default; OAuth-Flows laufen
|
|
210
|
-
// weiter, nur ohne tainted URL. Symptom-Diagnose via Console-Warning.
|
|
211
|
-
// eslint-disable-next-line no-console
|
|
212
|
-
console.warn(
|
|
213
|
-
`[startSocialLogin] callbackUrl cross-origin rejected: ${candidate.origin} != ${originUrl.origin}. Falling back to /login.`
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
} catch {
|
|
217
|
-
// Invalid URL → fallback, gleiche Rationale.
|
|
218
|
-
// eslint-disable-next-line no-console
|
|
219
|
-
console.warn('[startSocialLogin] callbackUrl is not a valid URL — falling back to /login.');
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
submitSocialRedirectForm({ provider, callbackUrl, csrfToken, process });
|
|
223
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { extractErrorInfo } from './auth-errors';
|
|
2
|
-
|
|
3
|
-
export function resolveErrorText(i18n, err, defaultKey) {
|
|
4
|
-
// API helpers normally throw normalized errors. Keeping this small boundary
|
|
5
|
-
// adapter also makes the resolver safe for callers that pass an Axios error
|
|
6
|
-
// directly (for example, an app-level wrapper around a kit API call).
|
|
7
|
-
const info = err?.response ? extractErrorInfo(err) : null;
|
|
8
|
-
const resolvedErr = info
|
|
9
|
-
? {
|
|
10
|
-
...err,
|
|
11
|
-
code: info.code || err.code,
|
|
12
|
-
field: info.field,
|
|
13
|
-
i18nKey: info.i18nKey,
|
|
14
|
-
backendMessage: info.message,
|
|
15
|
-
}
|
|
16
|
-
: err;
|
|
17
|
-
const exists = (key) => Boolean(key) && i18n.exists(key);
|
|
18
|
-
|
|
19
|
-
if (!resolvedErr) return i18n.t(defaultKey);
|
|
20
|
-
if (exists(resolvedErr.i18nKey)) return i18n.t(resolvedErr.i18nKey);
|
|
21
|
-
if (resolvedErr.field && resolvedErr.code) {
|
|
22
|
-
const fieldKey = `Auth.field.${resolvedErr.field}.${resolvedErr.code}`;
|
|
23
|
-
if (exists(fieldKey)) return i18n.t(fieldKey);
|
|
24
|
-
}
|
|
25
|
-
if (exists(resolvedErr.code)) return i18n.t(resolvedErr.code);
|
|
26
|
-
if (resolvedErr.backendMessage) return resolvedErr.backendMessage;
|
|
27
|
-
return i18n.t(defaultKey);
|
|
28
|
-
}
|
package/src/utils/webauthn.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
// src/utils/webauthn.js
|
|
2
|
-
export function bufferToBase64URL(buffer) {
|
|
3
|
-
const bytes = new Uint8Array(buffer);
|
|
4
|
-
let str = '';
|
|
5
|
-
for (const char of bytes) {
|
|
6
|
-
str += String.fromCharCode(char);
|
|
7
|
-
}
|
|
8
|
-
const base64 = btoa(str);
|
|
9
|
-
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function serializeCredential(credential) {
|
|
13
|
-
const p = {
|
|
14
|
-
id: credential.id,
|
|
15
|
-
rawId: bufferToBase64URL(credential.rawId),
|
|
16
|
-
type: credential.type,
|
|
17
|
-
response: {
|
|
18
|
-
clientDataJSON: bufferToBase64URL(credential.response.clientDataJSON),
|
|
19
|
-
},
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
if (credential.response.attestationObject) {
|
|
23
|
-
p.response.attestationObject = bufferToBase64URL(credential.response.attestationObject);
|
|
24
|
-
}
|
|
25
|
-
if (credential.response.authenticatorData) {
|
|
26
|
-
p.response.authenticatorData = bufferToBase64URL(credential.response.authenticatorData);
|
|
27
|
-
}
|
|
28
|
-
if (credential.response.signature) {
|
|
29
|
-
p.response.signature = bufferToBase64URL(credential.response.signature);
|
|
30
|
-
}
|
|
31
|
-
if (credential.response.userHandle) {
|
|
32
|
-
p.response.userHandle = bufferToBase64URL(credential.response.userHandle);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (typeof credential.getClientExtensionResults === 'function') {
|
|
36
|
-
p.clientExtensionResults = credential.getClientExtensionResults();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return p;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function ensureWebAuthnSupport() {
|
|
43
|
-
if (
|
|
44
|
-
typeof window === 'undefined' ||
|
|
45
|
-
typeof navigator === 'undefined' ||
|
|
46
|
-
!window.PublicKeyCredential ||
|
|
47
|
-
!navigator.credentials
|
|
48
|
-
) {
|
|
49
|
-
throw new Error('Auth.PASSKEY_NOT_SUPPORTED');
|
|
50
|
-
}
|
|
51
|
-
}
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
-
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
5
|
-
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
|
6
|
-
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
|
7
|
-
|
|
8
|
-
vi.mock('react-i18next', () => {
|
|
9
|
-
const KEYS = {
|
|
10
|
-
'Account.TITLE': 'Account & Administration',
|
|
11
|
-
'Account.PAGE_TITLE': 'Account',
|
|
12
|
-
'Account.TAB_PROFILE': 'Profile',
|
|
13
|
-
'Account.TAB_SECURITY': 'Security',
|
|
14
|
-
'Account.GROUP_MY_ACCOUNT': 'My account',
|
|
15
|
-
'Profile.ACCEPT_PRIVACY_LABEL': 'Accept privacy statement',
|
|
16
|
-
'Profile.ACCEPT_COOKIES_LABEL': 'Accept convenience cookies',
|
|
17
|
-
'Profile.PRIVACY_STATEMENT_TITLE': 'Privacy statement',
|
|
18
|
-
'Profile.COOKIES_STATEMENT_TITLE': 'Cookie statement',
|
|
19
|
-
'Profile.SAVE_BUTTON': 'Save',
|
|
20
|
-
'Profile.PRIVACY_SECTION_TITLE': 'Privacy and cookies',
|
|
21
|
-
'Profile.USERNAME_LABEL': 'Username',
|
|
22
|
-
'Profile.EMAIL_LABEL': 'Email',
|
|
23
|
-
'Profile.FIRST_NAME_LABEL': 'First name',
|
|
24
|
-
'Profile.LAST_NAME_LABEL': 'Last name',
|
|
25
|
-
};
|
|
26
|
-
const t = (key, fallback) => KEYS[key] ?? fallback ?? key;
|
|
27
|
-
return {
|
|
28
|
-
useTranslation: () => ({
|
|
29
|
-
t,
|
|
30
|
-
i18n: { exists: (key) => Object.hasOwn(KEYS, key), t },
|
|
31
|
-
}),
|
|
32
|
-
};
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
vi.mock('../src/auth/authApi', () => ({
|
|
36
|
-
fetchAuthPolicy: vi.fn(),
|
|
37
|
-
fetchPrivacyStatement: vi.fn(),
|
|
38
|
-
fetchCookieStatement: vi.fn(),
|
|
39
|
-
updateUserProfile: vi.fn(),
|
|
40
|
-
}));
|
|
41
|
-
|
|
42
|
-
vi.mock('../src/components/SecurityComponent', () => ({ SecurityComponent: () => null }));
|
|
43
|
-
vi.mock('../src/components/UserListComponent', () => ({ UserListComponent: () => null }));
|
|
44
|
-
vi.mock('../src/components/UserInviteComponent', () => ({ UserInviteComponent: () => null }));
|
|
45
|
-
vi.mock('../src/components/AccessCodeManager', () => ({ AccessCodeManager: () => null }));
|
|
46
|
-
vi.mock('../src/components/AllowedEmailDomainsManager', () => ({ AllowedEmailDomainsManager: () => null }));
|
|
47
|
-
vi.mock('../src/components/RegistrationMethodsManager', () => ({ RegistrationMethodsManager: () => null }));
|
|
48
|
-
vi.mock('../src/components/AuthFactorRequirementCard', () => ({ AuthFactorRequirementCard: () => null }));
|
|
49
|
-
vi.mock('../src/components/AccessCodeSingleUseToggle', () => ({ AccessCodeSingleUseToggle: () => null }));
|
|
50
|
-
vi.mock('../src/components/QrSignupManager', () => ({ QrSignupManager: () => null }));
|
|
51
|
-
vi.mock('../src/components/QrSignupValidityManager', () => ({ QrSignupValidityManager: () => null }));
|
|
52
|
-
vi.mock('../src/components/SupportRecoveryRequestsTab', () => ({ SupportRecoveryRequestsTab: () => null }));
|
|
53
|
-
vi.mock('../src/components/BulkInviteCsvTab', () => ({ BulkInviteCsvTab: () => null }));
|
|
54
|
-
|
|
55
|
-
import { AuthContext } from '../src/auth/AuthContext';
|
|
56
|
-
import {
|
|
57
|
-
fetchCookieStatement,
|
|
58
|
-
fetchPrivacyStatement,
|
|
59
|
-
updateUserProfile,
|
|
60
|
-
} from '../src/auth/authApi';
|
|
61
|
-
import { AccountPage } from '../src/pages/AccountPage';
|
|
62
|
-
|
|
63
|
-
const theme = createTheme();
|
|
64
|
-
const user = {
|
|
65
|
-
email: 'person@example.com',
|
|
66
|
-
username: 'person',
|
|
67
|
-
first_name: 'Pat',
|
|
68
|
-
last_name: 'Example',
|
|
69
|
-
accepted_privacy_statement: false,
|
|
70
|
-
accepted_convenience_cookies: true,
|
|
71
|
-
is_superuser: false,
|
|
72
|
-
ui_permissions: {},
|
|
73
|
-
available_roles: [],
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
function setViewportWidth(width) {
|
|
77
|
-
window.matchMedia = vi.fn().mockImplementation((query) => ({
|
|
78
|
-
matches: query.includes('max-width') ? width <= 900 : width > 900,
|
|
79
|
-
media: query,
|
|
80
|
-
onchange: null,
|
|
81
|
-
addEventListener: vi.fn(),
|
|
82
|
-
removeEventListener: vi.fn(),
|
|
83
|
-
addListener: vi.fn(),
|
|
84
|
-
removeListener: vi.fn(),
|
|
85
|
-
dispatchEvent: vi.fn(),
|
|
86
|
-
}));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function renderAccount({ showProfileStatements } = {}) {
|
|
90
|
-
const login = vi.fn();
|
|
91
|
-
return {
|
|
92
|
-
login,
|
|
93
|
-
...render(
|
|
94
|
-
<ThemeProvider theme={theme}>
|
|
95
|
-
<AuthContext.Provider value={{ user, login, loading: false }}>
|
|
96
|
-
<MemoryRouter initialEntries={['/account']}>
|
|
97
|
-
<Routes>
|
|
98
|
-
<Route
|
|
99
|
-
path="/account"
|
|
100
|
-
element={<AccountPage showProfileStatements={showProfileStatements} />}
|
|
101
|
-
/>
|
|
102
|
-
</Routes>
|
|
103
|
-
</MemoryRouter>
|
|
104
|
-
</AuthContext.Provider>
|
|
105
|
-
</ThemeProvider>,
|
|
106
|
-
),
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
describe('AccountPage profile statements', () => {
|
|
111
|
-
beforeEach(() => {
|
|
112
|
-
setViewportWidth(1280);
|
|
113
|
-
vi.mocked(fetchPrivacyStatement).mockResolvedValue('Privacy text');
|
|
114
|
-
vi.mocked(fetchCookieStatement).mockResolvedValue('Cookie text');
|
|
115
|
-
vi.mocked(updateUserProfile).mockResolvedValue({ ...user, accepted_privacy_statement: true });
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
afterEach(() => {
|
|
119
|
-
cleanup();
|
|
120
|
-
vi.clearAllMocks();
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('fetches and presents both statement panels when the prop is omitted', async () => {
|
|
124
|
-
renderAccount();
|
|
125
|
-
|
|
126
|
-
expect(await screen.findByText('Privacy statement')).toBeTruthy();
|
|
127
|
-
expect(screen.getByText('Cookie statement')).toBeTruthy();
|
|
128
|
-
await waitFor(() => {
|
|
129
|
-
expect(fetchPrivacyStatement).toHaveBeenCalledOnce();
|
|
130
|
-
expect(fetchCookieStatement).toHaveBeenCalledOnce();
|
|
131
|
-
});
|
|
132
|
-
expect(await screen.findByText('Privacy text')).toBeTruthy();
|
|
133
|
-
expect(screen.getByText('Cookie text')).toBeTruthy();
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('hides statements and skips requests while retaining editable consent submission', async () => {
|
|
137
|
-
const { login } = renderAccount({ showProfileStatements: false });
|
|
138
|
-
|
|
139
|
-
expect(screen.queryByText('Privacy statement')).toBeNull();
|
|
140
|
-
expect(screen.queryByText('Cookie statement')).toBeNull();
|
|
141
|
-
expect(fetchPrivacyStatement).not.toHaveBeenCalled();
|
|
142
|
-
expect(fetchCookieStatement).not.toHaveBeenCalled();
|
|
143
|
-
|
|
144
|
-
const privacyConsent = screen.getByRole('checkbox', { name: 'Accept privacy statement' });
|
|
145
|
-
const cookieConsent = screen.getByRole('checkbox', { name: 'Accept convenience cookies' });
|
|
146
|
-
expect(privacyConsent.checked).toBe(false);
|
|
147
|
-
expect(cookieConsent.checked).toBe(true);
|
|
148
|
-
fireEvent.click(privacyConsent);
|
|
149
|
-
fireEvent.click(cookieConsent);
|
|
150
|
-
fireEvent.click(screen.getByRole('button', { name: 'Save' }));
|
|
151
|
-
|
|
152
|
-
await waitFor(() => {
|
|
153
|
-
expect(updateUserProfile).toHaveBeenCalledWith({
|
|
154
|
-
first_name: 'Pat',
|
|
155
|
-
last_name: 'Example',
|
|
156
|
-
accepted_privacy_statement: true,
|
|
157
|
-
accepted_convenience_cookies: false,
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
expect(login).toHaveBeenCalledOnce();
|
|
161
|
-
});
|
|
162
|
-
});
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
-
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
|
5
|
-
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
|
6
|
-
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
|
|
7
|
-
|
|
8
|
-
vi.mock('react-i18next', () => {
|
|
9
|
-
const KEYS = {
|
|
10
|
-
'Account.TITLE': 'Account & Administration',
|
|
11
|
-
'Account.PAGE_TITLE': 'Account',
|
|
12
|
-
'Account.TAB_PROFILE': 'Profile',
|
|
13
|
-
'Account.TAB_SECURITY': 'Security',
|
|
14
|
-
'Account.TAB_USERS': 'Users',
|
|
15
|
-
'Account.TAB_INVITE': 'Invite',
|
|
16
|
-
'Account.TAB_SUPPORT': 'Support',
|
|
17
|
-
'Account.GROUP_MY_ACCOUNT': 'My account',
|
|
18
|
-
'Account.GROUP_MANAGEMENT': 'Management',
|
|
19
|
-
'Account.GROUP_HELP': 'Help',
|
|
20
|
-
'Account.GROUP_MORE': 'More',
|
|
21
|
-
'SectionNav.TITLE': 'Switch section',
|
|
22
|
-
'SectionNav.TRIGGER_EYEBROW': 'Section',
|
|
23
|
-
'SectionNav.LAST_OPENED': 'Last opened',
|
|
24
|
-
};
|
|
25
|
-
const t = (key, fallback) => KEYS[key] ?? fallback ?? key;
|
|
26
|
-
return {
|
|
27
|
-
useTranslation: () => ({
|
|
28
|
-
t,
|
|
29
|
-
// resolveErrorText (AUTH-10) reads i18n.exists/i18n.t off the object
|
|
30
|
-
// useTranslation() returns — this mock must carry both, not just `t`.
|
|
31
|
-
i18n: { exists: (key) => Object.hasOwn(KEYS, key), t },
|
|
32
|
-
}),
|
|
33
|
-
};
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
vi.mock('../src/auth/authApi', () => ({
|
|
37
|
-
fetchAuthPolicy: vi.fn(() => new Promise(() => {})),
|
|
38
|
-
updateUserProfile: vi.fn(),
|
|
39
|
-
}));
|
|
40
|
-
|
|
41
|
-
vi.mock('../src/components/ProfileComponent', () => ({
|
|
42
|
-
ProfileComponent: () => <div>Profile content</div>,
|
|
43
|
-
}));
|
|
44
|
-
vi.mock('../src/components/SecurityComponent', () => ({
|
|
45
|
-
SecurityComponent: () => <div>Security content</div>,
|
|
46
|
-
}));
|
|
47
|
-
vi.mock('../src/components/UserListComponent', () => ({
|
|
48
|
-
UserListComponent: () => <div>Users content</div>,
|
|
49
|
-
}));
|
|
50
|
-
vi.mock('../src/components/UserInviteComponent', () => ({ UserInviteComponent: () => null }));
|
|
51
|
-
vi.mock('../src/components/AccessCodeManager', () => ({ AccessCodeManager: () => null }));
|
|
52
|
-
vi.mock('../src/components/AllowedEmailDomainsManager', () => ({ AllowedEmailDomainsManager: () => null }));
|
|
53
|
-
vi.mock('../src/components/RegistrationMethodsManager', () => ({ RegistrationMethodsManager: () => null }));
|
|
54
|
-
vi.mock('../src/components/AuthFactorRequirementCard', () => ({ AuthFactorRequirementCard: () => null }));
|
|
55
|
-
vi.mock('../src/components/AccessCodeSingleUseToggle', () => ({ AccessCodeSingleUseToggle: () => null }));
|
|
56
|
-
vi.mock('../src/components/QrSignupManager', () => ({ QrSignupManager: () => null }));
|
|
57
|
-
vi.mock('../src/components/QrSignupValidityManager', () => ({ QrSignupValidityManager: () => null }));
|
|
58
|
-
vi.mock('../src/components/SupportRecoveryRequestsTab', () => ({
|
|
59
|
-
SupportRecoveryRequestsTab: () => <div>Support content</div>,
|
|
60
|
-
}));
|
|
61
|
-
vi.mock('../src/components/BulkInviteCsvTab', () => ({ BulkInviteCsvTab: () => null }));
|
|
62
|
-
|
|
63
|
-
import { AuthContext } from '../src/auth/AuthContext';
|
|
64
|
-
import { AccountPage } from '../src/pages/AccountPage';
|
|
65
|
-
|
|
66
|
-
const theme = createTheme();
|
|
67
|
-
const superuser = {
|
|
68
|
-
email: 'admin@example.com',
|
|
69
|
-
is_superuser: true,
|
|
70
|
-
ui_permissions: {},
|
|
71
|
-
available_roles: [],
|
|
72
|
-
};
|
|
73
|
-
const notificationsTab = {
|
|
74
|
-
value: 'notifications',
|
|
75
|
-
label: 'Notifications',
|
|
76
|
-
render: () => <div>Notifications content</div>,
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
function setViewportWidth(width) {
|
|
80
|
-
window.matchMedia = vi.fn().mockImplementation((query) => {
|
|
81
|
-
const maxWidth = query.match(/max-width:(\d+(?:\.\d+)?)px/)?.[1];
|
|
82
|
-
const minWidth = query.match(/min-width:(\d+(?:\.\d+)?)px/)?.[1];
|
|
83
|
-
const matches = maxWidth
|
|
84
|
-
? width <= Number(maxWidth)
|
|
85
|
-
: minWidth
|
|
86
|
-
? width >= Number(minWidth)
|
|
87
|
-
: false;
|
|
88
|
-
return {
|
|
89
|
-
matches,
|
|
90
|
-
media: query,
|
|
91
|
-
onchange: null,
|
|
92
|
-
addEventListener: vi.fn(),
|
|
93
|
-
removeEventListener: vi.fn(),
|
|
94
|
-
addListener: vi.fn(),
|
|
95
|
-
removeListener: vi.fn(),
|
|
96
|
-
dispatchEvent: vi.fn(),
|
|
97
|
-
};
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function LocationProbe() {
|
|
102
|
-
const location = useLocation();
|
|
103
|
-
return <output data-testid="location-search">{location.search}</output>;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function renderAccount({ user = superuser, route = '/account', extraTabs = [notificationsTab] } = {}) {
|
|
107
|
-
return render(
|
|
108
|
-
<ThemeProvider theme={theme}>
|
|
109
|
-
<AuthContext.Provider value={{ user, login: vi.fn(), loading: false }}>
|
|
110
|
-
<MemoryRouter initialEntries={[route]}>
|
|
111
|
-
<Routes>
|
|
112
|
-
<Route
|
|
113
|
-
path="/account"
|
|
114
|
-
element={(
|
|
115
|
-
<>
|
|
116
|
-
<AccountPage extraTabs={extraTabs} />
|
|
117
|
-
<LocationProbe />
|
|
118
|
-
</>
|
|
119
|
-
)}
|
|
120
|
-
/>
|
|
121
|
-
</Routes>
|
|
122
|
-
</MemoryRouter>
|
|
123
|
-
</AuthContext.Provider>
|
|
124
|
-
</ThemeProvider>,
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
describe('AccountPage section navigation', () => {
|
|
129
|
-
beforeEach(() => setViewportWidth(375));
|
|
130
|
-
|
|
131
|
-
afterEach(() => {
|
|
132
|
-
cleanup();
|
|
133
|
-
vi.restoreAllMocks();
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('makes all six cockpit-shaped sections reachable at 375px without a horizontal strip', () => {
|
|
137
|
-
const { container } = renderAccount();
|
|
138
|
-
const trigger = screen.getByRole('button', { name: /Section Profile/i });
|
|
139
|
-
|
|
140
|
-
expect(container.querySelector('.MuiTabs-root')).toBeNull();
|
|
141
|
-
expect(getComputedStyle(trigger).width).toBe('100%');
|
|
142
|
-
fireEvent.click(trigger);
|
|
143
|
-
|
|
144
|
-
const sectionButtons = ['Profile', 'Security', 'Users', 'Invite', 'Support', 'Notifications']
|
|
145
|
-
.map((label) => screen.getByRole('button', { name: label }));
|
|
146
|
-
expect(sectionButtons).toHaveLength(6);
|
|
147
|
-
sectionButtons.forEach((item) => {
|
|
148
|
-
expect(getComputedStyle(item).position).not.toBe('absolute');
|
|
149
|
-
});
|
|
150
|
-
expect(document.documentElement.scrollWidth - document.documentElement.clientWidth).toBe(0);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('keeps the ?tab= contract and updates it when a drawer item is selected', () => {
|
|
154
|
-
renderAccount({ route: '/account?tab=notifications' });
|
|
155
|
-
|
|
156
|
-
expect(screen.getByText('Notifications content')).toBeTruthy();
|
|
157
|
-
const trigger = screen.getByRole('button', { name: /Section Notifications/i });
|
|
158
|
-
fireEvent.click(trigger);
|
|
159
|
-
fireEvent.click(screen.getByRole('button', { name: 'Security' }));
|
|
160
|
-
|
|
161
|
-
expect(screen.getByText('Security content')).toBeTruthy();
|
|
162
|
-
expect(screen.getByTestId('location-search').textContent).toBe('?tab=security');
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('keeps permission-gated sections absent and falls an unpermitted query back to profile', () => {
|
|
166
|
-
renderAccount({
|
|
167
|
-
user: {
|
|
168
|
-
email: 'person@example.com',
|
|
169
|
-
is_superuser: false,
|
|
170
|
-
ui_permissions: {},
|
|
171
|
-
available_roles: [],
|
|
172
|
-
},
|
|
173
|
-
route: '/account?tab=users',
|
|
174
|
-
extraTabs: [],
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
expect(screen.getByText('Profile content')).toBeTruthy();
|
|
178
|
-
const trigger = screen.getByRole('button', { name: /Section Profile/i });
|
|
179
|
-
fireEvent.click(trigger);
|
|
180
|
-
expect(screen.getByRole('button', { name: 'Profile' })).toBeTruthy();
|
|
181
|
-
expect(screen.getByRole('button', { name: 'Security' })).toBeTruthy();
|
|
182
|
-
expect(screen.queryByRole('button', { name: 'Users' })).toBeNull();
|
|
183
|
-
expect(screen.queryByRole('button', { name: 'Invite' })).toBeNull();
|
|
184
|
-
expect(screen.queryByRole('button', { name: 'Support' })).toBeNull();
|
|
185
|
-
expect(screen.queryByText('Management')).toBeNull();
|
|
186
|
-
expect(screen.queryByText('Help')).toBeNull();
|
|
187
|
-
expect(screen.queryByText('More')).toBeNull();
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('uses the desktop grouped grid at 1280px with no tab strip left behind', () => {
|
|
191
|
-
setViewportWidth(1280);
|
|
192
|
-
const { container } = renderAccount();
|
|
193
|
-
const grid = container.querySelector('[data-section-nav-mode="desktop"]');
|
|
194
|
-
|
|
195
|
-
expect(grid).toBeTruthy();
|
|
196
|
-
expect(getComputedStyle(grid).gridTemplateColumns).toBe('280px minmax(0, 1fr)');
|
|
197
|
-
expect(container.querySelector('.MuiTabs-root')).toBeNull();
|
|
198
|
-
expect(container.querySelector('nav')).toBeTruthy();
|
|
199
|
-
});
|
|
200
|
-
});
|