@micha.bigler/ui-core-micha 3.8.0 → 3.9.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 +18 -0
- package/WORK_ORDERS.md +1 -0
- package/dist/auth/apiClient.js +8 -0
- package/dist/components/AccessCodeManager.js +5 -5
- package/dist/components/AccessCodeSingleUseToggle.js +3 -2
- package/dist/components/AllowedEmailDomainsManager.js +3 -2
- package/dist/components/AuthFactorRequirementCard.js +3 -2
- package/dist/components/BulkInviteCsvTab.js +3 -2
- package/dist/components/MFAComponent.js +9 -8
- package/dist/components/MfaLoginComponent.js +7 -6
- package/dist/components/PasskeysComponent.js +7 -6
- package/dist/components/ProfileComponent.js +4 -3
- package/dist/components/QrSignupManager.js +4 -3
- package/dist/components/QrSignupValidityManager.js +3 -2
- package/dist/components/RegistrationMethodsManager.js +3 -2
- package/dist/components/SecurityComponent.js +5 -4
- package/dist/components/SupportRecoveryRequestsTab.js +8 -7
- package/dist/components/UserInviteComponent.js +3 -2
- package/dist/components/UserListComponent.js +7 -6
- package/dist/i18n/authTranslations.js +420 -0
- package/dist/pages/AccountPage.js +3 -2
- package/dist/pages/LoginPage.js +10 -9
- package/dist/pages/PasswordChangePage.js +4 -3
- package/dist/pages/PasswordInvitePage.js +7 -6
- package/dist/pages/PasswordResetRequestPage.js +5 -4
- package/dist/pages/SignUpPage.js +7 -6
- package/dist/pages/SignupConfirmPage.js +7 -6
- package/dist/utils/auth-errors.js +45 -8
- package/dist/utils/resolveErrorText.js +24 -0
- package/package.json +1 -1
- package/src/auth/apiClient.jsx +9 -0
- package/src/components/AccessCodeManager.jsx +5 -5
- package/src/components/AccessCodeSingleUseToggle.jsx +3 -2
- package/src/components/AllowedEmailDomainsManager.jsx +3 -2
- package/src/components/AuthFactorRequirementCard.jsx +3 -2
- package/src/components/BulkInviteCsvTab.jsx +3 -2
- package/src/components/MFAComponent.jsx +9 -8
- package/src/components/MfaLoginComponent.jsx +7 -6
- package/src/components/PasskeysComponent.jsx +7 -6
- package/src/components/ProfileComponent.jsx +4 -3
- package/src/components/QrSignupManager.jsx +4 -3
- package/src/components/QrSignupValidityManager.jsx +3 -2
- package/src/components/RegistrationMethodsManager.jsx +3 -2
- package/src/components/SecurityComponent.jsx +5 -4
- package/src/components/SupportRecoveryRequestsTab.jsx +9 -8
- package/src/components/UserInviteComponent.jsx +3 -2
- package/src/components/UserListComponent.jsx +7 -6
- package/src/i18n/authTranslations.ts +421 -0
- package/src/pages/AccountPage.jsx +3 -4
- package/src/pages/LoginPage.jsx +10 -9
- package/src/pages/PasswordChangePage.jsx +4 -3
- package/src/pages/PasswordInvitePage.jsx +7 -6
- package/src/pages/PasswordResetRequestPage.jsx +5 -4
- package/src/pages/SignUpPage.jsx +7 -6
- package/src/pages/SignupConfirmPage.jsx +7 -6
- package/src/utils/auth-errors.js +44 -7
- package/src/utils/resolveErrorText.js +28 -0
- package/tests/AccountPage.test.jsx +27 -20
- package/tests/PasswordInvitePage.test.jsx +28 -21
- package/tests/QrSignupManager.registrationContext.test.jsx +5 -1
- package/tests/SignUpPage.test.jsx +14 -3
- package/tests/UserListComponent.test.jsx +22 -15
- package/tests/apiClientLanguage.test.js +33 -0
- package/tests/authErrors.test.js +31 -0
- package/tests/errorCallSiteGuard.test.js +32 -0
- package/tests/loginPageErrorResolution.test.jsx +102 -0
- package/tests/resolveErrorText.test.js +72 -0
- package/work-orders/UCM-AUTH-10.md +490 -0
package/dist/pages/LoginPage.js
CHANGED
|
@@ -13,12 +13,13 @@ import { loginWithPasskey, startSocialLogin } from '../utils/authService';
|
|
|
13
13
|
// Components
|
|
14
14
|
import { LoginForm } from '../components/LoginForm';
|
|
15
15
|
import { MfaLoginComponent } from '../components/MfaLoginComponent';
|
|
16
|
+
import { resolveErrorText } from '../utils/resolveErrorText';
|
|
16
17
|
export const LOGIN_PAGE_ALERT_SX = { mb: 2 };
|
|
17
18
|
export function LoginPage() {
|
|
18
19
|
const navigate = useNavigate();
|
|
19
20
|
const location = useLocation();
|
|
20
21
|
const { user, loading, login, authMethods } = useContext(AuthContext);
|
|
21
|
-
const { t } = useTranslation();
|
|
22
|
+
const { t, i18n } = useTranslation();
|
|
22
23
|
// State
|
|
23
24
|
const [step, setStep] = useState('credentials'); // 'credentials' | 'mfa'
|
|
24
25
|
const [submitting, setSubmitting] = useState(false);
|
|
@@ -77,7 +78,7 @@ export function LoginPage() {
|
|
|
77
78
|
useEffect(() => {
|
|
78
79
|
const socialError = params.get('error') || params.get('social');
|
|
79
80
|
if (socialError) {
|
|
80
|
-
setErrorKey('Auth.SOCIAL_LOGIN_FAILED');
|
|
81
|
+
setErrorKey(t('Auth.SOCIAL_LOGIN_FAILED'));
|
|
81
82
|
}
|
|
82
83
|
}, [location.search]);
|
|
83
84
|
// S164 handoff: on `#recovery=ok`, pull the plaintext token out of the
|
|
@@ -112,14 +113,14 @@ export function LoginPage() {
|
|
|
112
113
|
window.history.replaceState(null, '', cleanUrl);
|
|
113
114
|
}
|
|
114
115
|
if (recoveryStatus === 'expired') {
|
|
115
|
-
setErrorKey('Auth.RECOVERY_TOKEN_EXPIRED');
|
|
116
|
+
setErrorKey(t('Auth.RECOVERY_TOKEN_EXPIRED'));
|
|
116
117
|
setRecoveryToken(null);
|
|
117
118
|
return;
|
|
118
119
|
}
|
|
119
120
|
if (recoveryStatus !== 'ok') {
|
|
120
121
|
// Catches `invalid` and any unknown value (including legacy email
|
|
121
122
|
// links that carried the plaintext token before S164).
|
|
122
|
-
setErrorKey('Auth.RECOVERY_TOKEN_INVALID');
|
|
123
|
+
setErrorKey(t('Auth.RECOVERY_TOKEN_INVALID'));
|
|
123
124
|
setRecoveryToken(null);
|
|
124
125
|
return;
|
|
125
126
|
}
|
|
@@ -131,7 +132,7 @@ export function LoginPage() {
|
|
|
131
132
|
return;
|
|
132
133
|
if (!token) {
|
|
133
134
|
// Session entry missing or already consumed — treat as invalid.
|
|
134
|
-
setErrorKey('Auth.RECOVERY_TOKEN_INVALID');
|
|
135
|
+
setErrorKey(t('Auth.RECOVERY_TOKEN_INVALID'));
|
|
135
136
|
setRecoveryToken(null);
|
|
136
137
|
return;
|
|
137
138
|
}
|
|
@@ -140,7 +141,7 @@ export function LoginPage() {
|
|
|
140
141
|
.catch((err) => {
|
|
141
142
|
if (cancelled)
|
|
142
143
|
return;
|
|
143
|
-
setErrorKey((
|
|
144
|
+
setErrorKey(resolveErrorText(i18n, err, 'Auth.RECOVERY_TOKEN_INVALID'));
|
|
144
145
|
setRecoveryToken(null);
|
|
145
146
|
})
|
|
146
147
|
.finally(() => {
|
|
@@ -190,7 +191,7 @@ export function LoginPage() {
|
|
|
190
191
|
}
|
|
191
192
|
}
|
|
192
193
|
catch (err) {
|
|
193
|
-
setErrorKey(err
|
|
194
|
+
setErrorKey(resolveErrorText(i18n, err, 'Auth.LOGIN_FAILED'));
|
|
194
195
|
}
|
|
195
196
|
finally {
|
|
196
197
|
setSubmitting(false);
|
|
@@ -207,7 +208,7 @@ export function LoginPage() {
|
|
|
207
208
|
catch (err) {
|
|
208
209
|
// 'Auth.PASSKEY_CANCELLED' is generic, maybe ignore visually or show specific hint
|
|
209
210
|
if (err.code !== 'Auth.PASSKEY_CANCELLED') {
|
|
210
|
-
setErrorKey(err
|
|
211
|
+
setErrorKey(resolveErrorText(i18n, err, 'Auth.PASSKEY_FAILED'));
|
|
211
212
|
}
|
|
212
213
|
}
|
|
213
214
|
finally {
|
|
@@ -245,7 +246,7 @@ export function LoginPage() {
|
|
|
245
246
|
const twoFactorRequired = Boolean(authMethods === null || authMethods === void 0 ? void 0 : authMethods.two_factor_required)
|
|
246
247
|
|| Number((authMethods === null || authMethods === void 0 ? void 0 : authMethods.required_auth_factor_count) || 1) >= 2;
|
|
247
248
|
// --- Render ---
|
|
248
|
-
return (_jsxs(NarrowPage, { title: t('Auth.PAGE_LOGIN_TITLE'), subtitle: t('Auth.PAGE_LOGIN_SUBTITLE'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t('Auth.PAGE_LOGIN_TITLE')] }) }), errorKey && (_jsx(Alert, { severity: "error", sx: LOGIN_PAGE_ALERT_SX, children:
|
|
249
|
+
return (_jsxs(NarrowPage, { title: t('Auth.PAGE_LOGIN_TITLE'), subtitle: t('Auth.PAGE_LOGIN_SUBTITLE'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t('Auth.PAGE_LOGIN_TITLE')] }) }), errorKey && (_jsx(Alert, { severity: "error", sx: LOGIN_PAGE_ALERT_SX, children: errorKey })), recoveryFetching && !errorKey && (_jsx(Alert, { severity: "info", sx: LOGIN_PAGE_ALERT_SX, children: t('Auth.RECOVERY_LINK_VALIDATING', 'Validating recovery link…') })), recoveryToken && !errorKey && (_jsx(Alert, { severity: "info", sx: LOGIN_PAGE_ALERT_SX, children: t('Auth.RECOVERY_LOGIN_WARNING', 'Recovery link validated. Please enter your password.') })), twoFactorRequired && !recoveryToken && !recoveryFetching && (_jsx(Alert, { severity: "info", sx: LOGIN_PAGE_ALERT_SX, children: t('Auth.TWO_FACTOR_REQUIRED_HINT', 'This app requires two authentication factors for full access.') })), step === 'credentials' && (_jsx(LoginForm, { onSubmit: passwordLoginEnabled ? handleSubmitCredentials : null, onForgotPassword: passwordResetEnabled ? () => navigate('/reset-request-password') : null, onSocialLogin: socialLoginEnabled ? (provider) => startSocialLogin(provider) : null, socialProviders: socialProviders, onPasskeyLogin: passkeyLoginEnabled ? handlePasskeyLoginInitial : null, onSignUp: signupEnabled ? () => navigate('/signup') : null,
|
|
249
250
|
// Block submit while the handoff is in flight so the user does not
|
|
250
251
|
// race the recovery-token fetch with a normal password POST.
|
|
251
252
|
disabled: submitting || recoveryFetching, initialIdentifier: recoveryEmail })), step === 'mfa' && mfaState && (_jsx(Box, { children: _jsx(MfaLoginComponent, { availableTypes: mfaState.availableTypes, identifier: mfaState.identifier, onSuccess: handleMfaSuccess, onCancel: handleMfaCancel }) }))] }));
|
|
@@ -7,8 +7,9 @@ import { useTranslation } from 'react-i18next';
|
|
|
7
7
|
import { NarrowPage } from '../layout/PageLayout';
|
|
8
8
|
import { PasswordChangeForm } from '../components/PasswordChangeForm';
|
|
9
9
|
import { changePassword } from '../auth/authApi';
|
|
10
|
+
import { resolveErrorText } from '../utils/resolveErrorText';
|
|
10
11
|
export function PasswordChangePage() {
|
|
11
|
-
const { t } = useTranslation();
|
|
12
|
+
const { t, i18n } = useTranslation();
|
|
12
13
|
const [submitting, setSubmitting] = useState(false);
|
|
13
14
|
const [successKey, setSuccessKey] = useState(null);
|
|
14
15
|
const [errorKey, setErrorKey] = useState(null);
|
|
@@ -21,11 +22,11 @@ export function PasswordChangePage() {
|
|
|
21
22
|
setSuccessKey('Auth.RESET_PASSWORD_SUCCESS');
|
|
22
23
|
}
|
|
23
24
|
catch (err) {
|
|
24
|
-
setErrorKey(err
|
|
25
|
+
setErrorKey(resolveErrorText(i18n, err, 'Auth.PASSWORD_CHANGE_FAILED'));
|
|
25
26
|
}
|
|
26
27
|
finally {
|
|
27
28
|
setSubmitting(false);
|
|
28
29
|
}
|
|
29
30
|
};
|
|
30
|
-
return (_jsxs(NarrowPage, { title: t('Auth.CHANGE_PASSWORD_BUTTON'), subtitle: t('Auth.PAGE_CHANGE_PASSWORD_SUBTITLE'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t('Auth.CHANGE_PASSWORD_BUTTON')] }) }), successKey && (_jsx(Typography, { color: "primary", gutterBottom: true, children: t(successKey) })), errorKey && (_jsx(Typography, { color: "error", gutterBottom: true, children:
|
|
31
|
+
return (_jsxs(NarrowPage, { title: t('Auth.CHANGE_PASSWORD_BUTTON'), subtitle: t('Auth.PAGE_CHANGE_PASSWORD_SUBTITLE'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t('Auth.CHANGE_PASSWORD_BUTTON')] }) }), successKey && (_jsx(Typography, { color: "primary", gutterBottom: true, children: t(successKey) })), errorKey && (_jsx(Typography, { color: "error", gutterBottom: true, children: errorKey })), _jsx(PasswordChangeForm, { onSubmit: handleSubmit, submitting: submitting })] }));
|
|
31
32
|
}
|
|
@@ -8,11 +8,12 @@ import { useTranslation } from 'react-i18next';
|
|
|
8
8
|
import { NarrowPage } from '../layout/PageLayout';
|
|
9
9
|
import { PasswordSetForm } from '../components/PasswordSetForm';
|
|
10
10
|
import { verifyResetToken, setNewPassword } from '../auth/authApi';
|
|
11
|
+
import { resolveErrorText } from '../utils/resolveErrorText';
|
|
11
12
|
export function PasswordInvitePage() {
|
|
12
13
|
const { uid, token } = useParams();
|
|
13
14
|
const location = useLocation();
|
|
14
15
|
const navigate = useNavigate();
|
|
15
|
-
const { t } = useTranslation();
|
|
16
|
+
const { t, i18n } = useTranslation();
|
|
16
17
|
const searchParams = new URLSearchParams(location.search);
|
|
17
18
|
const nextPath = searchParams.get('next');
|
|
18
19
|
const [submitting, setSubmitting] = useState(false);
|
|
@@ -32,7 +33,7 @@ export function PasswordInvitePage() {
|
|
|
32
33
|
: 'Auth.PAGE_RESET_PASSWORD_SUBTITLE';
|
|
33
34
|
useEffect(() => {
|
|
34
35
|
if (!uid || !token) {
|
|
35
|
-
setErrorKey('Auth.RESET_LINK_INVALID');
|
|
36
|
+
setErrorKey(t('Auth.RESET_LINK_INVALID'));
|
|
36
37
|
setChecked(true);
|
|
37
38
|
return;
|
|
38
39
|
}
|
|
@@ -42,7 +43,7 @@ export function PasswordInvitePage() {
|
|
|
42
43
|
setChecked(true);
|
|
43
44
|
}
|
|
44
45
|
catch (err) {
|
|
45
|
-
setErrorKey(err
|
|
46
|
+
setErrorKey(resolveErrorText(i18n, err, 'Auth.RESET_LINK_INVALID'));
|
|
46
47
|
setChecked(true);
|
|
47
48
|
}
|
|
48
49
|
};
|
|
@@ -98,13 +99,13 @@ export function PasswordInvitePage() {
|
|
|
98
99
|
// only a password rejection keeps the form; a link problem must still
|
|
99
100
|
// end in the same invalid-link state the initial check produces.
|
|
100
101
|
if (err.code === 'Auth.RESET_LINK_INVALID') {
|
|
101
|
-
setErrorKey(err.
|
|
102
|
+
setErrorKey(resolveErrorText(i18n, err, 'Auth.RESET_LINK_INVALID'));
|
|
102
103
|
}
|
|
103
104
|
else {
|
|
104
105
|
const responseMessages = ((_a = err.raw) === null || _a === void 0 ? void 0 : _a.messages) || ((_c = (_b = err.response) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.messages);
|
|
105
106
|
setSubmitErrorKey(Array.isArray(responseMessages) && responseMessages.length > 0
|
|
106
107
|
? responseMessages.join(' ')
|
|
107
|
-
:
|
|
108
|
+
: resolveErrorText(i18n, err, 'Auth.RESET_PASSWORD_FAILED'));
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
111
|
finally {
|
|
@@ -115,5 +116,5 @@ export function PasswordInvitePage() {
|
|
|
115
116
|
if (!checked && !errorKey) {
|
|
116
117
|
return (_jsx(NarrowPage, { title: t('Auth.PAGE_CHECKING_LINK_TITLE'), children: _jsx(Typography, { children: t('Auth.PAGE_CHECKING_LINK_TEXT') }) }));
|
|
117
118
|
}
|
|
118
|
-
return (_jsxs(NarrowPage, { title: t(pageTitleKey), subtitle: t(pageSubtitleKey), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t(pageTitleKey)] }) }), errorKey && (_jsx(Typography, { color: "error", gutterBottom: true, children:
|
|
119
|
+
return (_jsxs(NarrowPage, { title: t(pageTitleKey), subtitle: t(pageSubtitleKey), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t(pageTitleKey)] }) }), errorKey && (_jsx(Typography, { color: "error", gutterBottom: true, children: errorKey })), successKey && (_jsxs(_Fragment, { children: [_jsx(Typography, { color: "primary", gutterBottom: true, children: t(successKey) }), _jsx(Button, { variant: "contained", onClick: () => navigate(continueTarget), children: t('Auth.SIGNUP_GO_TO_LOGIN') })] })), submitErrorKey && !successKey && (_jsx(Typography, { color: "error", gutterBottom: true, children: submitErrorKey })), !successKey && !errorKey && (_jsx(PasswordSetForm, { onSubmit: handleSubmit, submitting: submitting }))] }));
|
|
119
120
|
}
|
|
@@ -7,8 +7,9 @@ import { useTranslation } from 'react-i18next';
|
|
|
7
7
|
import { NarrowPage } from '../layout/PageLayout';
|
|
8
8
|
import { requestPasswordReset } from '../auth/authApi';
|
|
9
9
|
import { PasswordResetRequestForm } from '../components/PasswordResetRequestForm';
|
|
10
|
+
import { resolveErrorText } from '../utils/resolveErrorText';
|
|
10
11
|
export function PasswordResetRequestPage() {
|
|
11
|
-
const { t } = useTranslation();
|
|
12
|
+
const { t, i18n } = useTranslation();
|
|
12
13
|
const [submitting, setSubmitting] = useState(false);
|
|
13
14
|
const [successKey, setSuccessKey] = useState(null);
|
|
14
15
|
const [errorKey, setErrorKey] = useState(null);
|
|
@@ -16,7 +17,7 @@ export function PasswordResetRequestPage() {
|
|
|
16
17
|
setSuccessKey(null);
|
|
17
18
|
setErrorKey(null);
|
|
18
19
|
if (!email) {
|
|
19
|
-
setErrorKey('Auth.EMAIL_REQUIRED');
|
|
20
|
+
setErrorKey(t('Auth.EMAIL_REQUIRED'));
|
|
20
21
|
return;
|
|
21
22
|
}
|
|
22
23
|
setSubmitting(true);
|
|
@@ -26,11 +27,11 @@ export function PasswordResetRequestPage() {
|
|
|
26
27
|
setSuccessKey('Auth.RESET_REQUEST_ACCEPTED');
|
|
27
28
|
}
|
|
28
29
|
catch (err) {
|
|
29
|
-
setErrorKey(err
|
|
30
|
+
setErrorKey(resolveErrorText(i18n, err, 'Auth.RESET_REQUEST_FAILED'));
|
|
30
31
|
}
|
|
31
32
|
finally {
|
|
32
33
|
setSubmitting(false);
|
|
33
34
|
}
|
|
34
35
|
};
|
|
35
|
-
return (_jsxs(NarrowPage, { title: t('Auth.PAGE_RESET_PASSWORD_TITLE'), subtitle: t('Auth.PAGE_RESET_REQUEST_SUBTITLE'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t('Auth.PAGE_RESET_PASSWORD_TITLE')] }) }), successKey && (_jsx(Typography, { color: "primary", gutterBottom: true, children: t(successKey) })), errorKey && (_jsx(Typography, { color: "error", gutterBottom: true, children:
|
|
36
|
+
return (_jsxs(NarrowPage, { title: t('Auth.PAGE_RESET_PASSWORD_TITLE'), subtitle: t('Auth.PAGE_RESET_REQUEST_SUBTITLE'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t('Auth.PAGE_RESET_PASSWORD_TITLE')] }) }), successKey && (_jsx(Typography, { color: "primary", gutterBottom: true, children: t(successKey) })), errorKey && (_jsx(Typography, { color: "error", gutterBottom: true, children: errorKey })), _jsx(PasswordResetRequestForm, { onSubmit: handleSubmit, submitting: submitting })] }));
|
|
36
37
|
}
|
package/dist/pages/SignUpPage.js
CHANGED
|
@@ -8,6 +8,7 @@ import { NarrowPage } from '../layout/PageLayout';
|
|
|
8
8
|
import { AuthContext } from '../auth/AuthContext';
|
|
9
9
|
import { submitRegistrationRequest } from '../auth/authApi';
|
|
10
10
|
import { TurnstileWidget } from '../components/TurnstileWidget';
|
|
11
|
+
import { resolveErrorText } from '../utils/resolveErrorText';
|
|
11
12
|
export const SIGN_UP_PAGE_ALERT_SX = { mb: 2 };
|
|
12
13
|
const TURNSTILE_SIGNUP_MODES = new Set([
|
|
13
14
|
'self_signup_open',
|
|
@@ -28,7 +29,7 @@ const MODE_SUBTITLES = {
|
|
|
28
29
|
export function SignUpPage() {
|
|
29
30
|
const navigate = useNavigate();
|
|
30
31
|
const location = useLocation();
|
|
31
|
-
const { t } = useTranslation();
|
|
32
|
+
const { t, i18n } = useTranslation();
|
|
32
33
|
const { authMethods } = useContext(AuthContext);
|
|
33
34
|
const signupModes = useMemo(() => {
|
|
34
35
|
const configured = Array.isArray(authMethods === null || authMethods === void 0 ? void 0 : authMethods.signup_modes)
|
|
@@ -75,15 +76,15 @@ export function SignUpPage() {
|
|
|
75
76
|
setSuccessKey(null);
|
|
76
77
|
setErrorKey(null);
|
|
77
78
|
if (!email) {
|
|
78
|
-
setErrorKey('Auth.EMAIL_REQUIRED');
|
|
79
|
+
setErrorKey(t('Auth.EMAIL_REQUIRED'));
|
|
79
80
|
return;
|
|
80
81
|
}
|
|
81
82
|
if (mode === 'self_signup_access_code' && !accessCode) {
|
|
82
|
-
setErrorKey('Auth.SIGNUP_ACCESS_CODE_REQUIRED');
|
|
83
|
+
setErrorKey(t('Auth.SIGNUP_ACCESS_CODE_REQUIRED'));
|
|
83
84
|
return;
|
|
84
85
|
}
|
|
85
86
|
if (mode === 'self_signup_qr' && !tokenFromUrl) {
|
|
86
|
-
setErrorKey('Auth.SIGNUP_QR_INVALID');
|
|
87
|
+
setErrorKey(t('Auth.SIGNUP_QR_INVALID'));
|
|
87
88
|
return;
|
|
88
89
|
}
|
|
89
90
|
if (turnstileRequired && !turnstileToken)
|
|
@@ -104,7 +105,7 @@ export function SignUpPage() {
|
|
|
104
105
|
setSuccessKey((res === null || res === void 0 ? void 0 : res.code) || 'Auth.INVITE_SENT');
|
|
105
106
|
}
|
|
106
107
|
catch (err) {
|
|
107
|
-
setErrorKey(err
|
|
108
|
+
setErrorKey(resolveErrorText(i18n, err, 'Auth.INVITE_FAILED'));
|
|
108
109
|
if (turnstileRequired) {
|
|
109
110
|
setTurnstileToken('');
|
|
110
111
|
(_a = turnstileWidgetRef.current) === null || _a === void 0 ? void 0 : _a.reset();
|
|
@@ -117,7 +118,7 @@ export function SignUpPage() {
|
|
|
117
118
|
const handleGoToLogin = () => {
|
|
118
119
|
navigate('/login');
|
|
119
120
|
};
|
|
120
|
-
return (_jsxs(NarrowPage, { title: t('Auth.LOGIN_SIGNUP_BUTTON'), subtitle: pageSubtitle, children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t('Auth.LOGIN_SIGNUP_BUTTON')] }) }), signupModes.length === 0 ? (_jsx(Alert, { severity: "info", sx: SIGN_UP_PAGE_ALERT_SX, children: t('Auth.SELF_SIGNUP_DISABLED', 'Self sign-up is currently disabled.') })) : (_jsxs(_Fragment, { children: [successKey && (_jsx(Alert, { severity: "success", sx: SIGN_UP_PAGE_ALERT_SX, children: t(successKey, { email }) })), errorKey && (_jsx(Alert, { severity: "error", sx: SIGN_UP_PAGE_ALERT_SX, children:
|
|
121
|
+
return (_jsxs(NarrowPage, { title: t('Auth.LOGIN_SIGNUP_BUTTON'), subtitle: pageSubtitle, children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t('Auth.LOGIN_SIGNUP_BUTTON')] }) }), signupModes.length === 0 ? (_jsx(Alert, { severity: "info", sx: SIGN_UP_PAGE_ALERT_SX, children: t('Auth.SELF_SIGNUP_DISABLED', 'Self sign-up is currently disabled.') })) : (_jsxs(_Fragment, { children: [successKey && (_jsx(Alert, { severity: "success", sx: SIGN_UP_PAGE_ALERT_SX, children: t(successKey, { email }) })), errorKey && (_jsx(Alert, { severity: "error", sx: SIGN_UP_PAGE_ALERT_SX, children: errorKey })), signupModes.length > 1 && (_jsx(Stack, { spacing: 1, sx: { mb: 2 }, children: _jsx(Stack, { spacing: 1, children: signupModes.map((entry) => (_jsx(Button, { variant: mode === entry ? 'contained' : 'outlined', onClick: () => setMode(entry), disabled: submitting, fullWidth: true, children: t(MODE_LABELS[entry] || entry, entry) }, entry))) }) })), _jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { display: 'flex', flexDirection: 'column', gap: 2 }, children: [_jsx(TextField, { label: t('Auth.EMAIL_LABEL'), type: "email", required: true, fullWidth: true, value: email, onChange: (e) => setEmail(e.target.value), disabled: submitting }), mode === 'self_signup_access_code' && (_jsx(TextField, { label: t('Auth.ACCESS_CODE_LABEL'), type: "text", required: true, fullWidth: true, value: accessCode, onChange: (e) => setAccessCode(e.target.value), disabled: submitting })), turnstileRequired && (_jsx(TurnstileWidget, { ref: turnstileWidgetRef, siteKey: authMethods.turnstile_site_key, onToken: setTurnstileToken })), _jsx(Button, { type: "submit", variant: "contained", disabled: submitting || (turnstileRequired && !turnstileToken), children: submitting
|
|
121
122
|
? t('Auth.SIGNUP_SUBMITTING')
|
|
122
123
|
: t('Auth.SIGNUP_SUBMIT') })] })] })), _jsx(Box, { sx: { mt: 3 }, children: _jsxs(Typography, { variant: "body2", children: [t('Auth.SIGNUP_ALREADY_HAVE_ACCOUNT'), ' ', _jsx(Button, { onClick: handleGoToLogin, variant: "text", size: "small", children: t('Auth.SIGNUP_GO_TO_LOGIN') })] }) })] }));
|
|
123
124
|
}
|
|
@@ -13,9 +13,10 @@ import { Helmet } from 'react-helmet';
|
|
|
13
13
|
import { useTranslation } from 'react-i18next';
|
|
14
14
|
import { NarrowPage } from '../layout/PageLayout';
|
|
15
15
|
import { confirmRegistration } from '../auth/authApi';
|
|
16
|
+
import { resolveErrorText } from '../utils/resolveErrorText';
|
|
16
17
|
export const SIGNUP_CONFIRM_PAGE_ALERT_SX = { mb: 2 };
|
|
17
18
|
export function SignupConfirmPage() {
|
|
18
|
-
const { t } = useTranslation();
|
|
19
|
+
const { t, i18n } = useTranslation();
|
|
19
20
|
const location = useLocation();
|
|
20
21
|
const navigate = useNavigate();
|
|
21
22
|
const tokenFromUrl = useMemo(() => {
|
|
@@ -30,15 +31,15 @@ export function SignupConfirmPage() {
|
|
|
30
31
|
event.preventDefault();
|
|
31
32
|
setErrorKey(null);
|
|
32
33
|
if (!tokenFromUrl) {
|
|
33
|
-
setErrorKey('Auth.PENDING_TOKEN_INVALID');
|
|
34
|
+
setErrorKey(t('Auth.PENDING_TOKEN_INVALID'));
|
|
34
35
|
return;
|
|
35
36
|
}
|
|
36
37
|
if (!password || password.length < 8) {
|
|
37
|
-
setErrorKey('Auth.PASSWORD_TOO_SHORT');
|
|
38
|
+
setErrorKey(t('Auth.PASSWORD_TOO_SHORT'));
|
|
38
39
|
return;
|
|
39
40
|
}
|
|
40
41
|
if (password !== confirmPassword) {
|
|
41
|
-
setErrorKey('Auth.PASSWORD_MISMATCH');
|
|
42
|
+
setErrorKey(t('Auth.PASSWORD_MISMATCH'));
|
|
42
43
|
return;
|
|
43
44
|
}
|
|
44
45
|
setSubmitting(true);
|
|
@@ -51,14 +52,14 @@ export function SignupConfirmPage() {
|
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
53
54
|
catch (err) {
|
|
54
|
-
setErrorKey((
|
|
55
|
+
setErrorKey(resolveErrorText(i18n, err, 'Auth.PENDING_TOKEN_INVALID'));
|
|
55
56
|
}
|
|
56
57
|
finally {
|
|
57
58
|
setSubmitting(false);
|
|
58
59
|
}
|
|
59
60
|
};
|
|
60
61
|
const tokenMissing = !tokenFromUrl;
|
|
61
|
-
return (_jsxs(NarrowPage, { title: t('Auth.SIGNUP_CONFIRM_TITLE', 'Confirm your registration'), subtitle: t('Auth.SIGNUP_CONFIRM_SUBTITLE', 'Choose a password to finish creating your account.'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t('Auth.SIGNUP_CONFIRM_TITLE', 'Confirm your registration')] }) }), tokenMissing ? (_jsxs(_Fragment, { children: [_jsx(Alert, { severity: "error", sx: SIGNUP_CONFIRM_PAGE_ALERT_SX, children: t('Auth.PENDING_TOKEN_INVALID', 'This confirmation link is invalid or expired.') }), _jsxs(Stack, { direction: "row", spacing: 1, sx: { mt: 1 }, children: [_jsx(Button, { onClick: () => navigate('/signup'), variant: "contained", children: t('Auth.SIGNUP_REQUEST_NEW', 'Request a new invitation') }), _jsx(Button, { onClick: () => navigate('/login'), variant: "text", children: t('Auth.SIGNUP_GO_TO_LOGIN', 'Go to login') })] })] })) : (_jsxs(_Fragment, { children: [errorKey && (_jsx(Alert, { severity: "error", sx: SIGNUP_CONFIRM_PAGE_ALERT_SX, children:
|
|
62
|
+
return (_jsxs(NarrowPage, { title: t('Auth.SIGNUP_CONFIRM_TITLE', 'Confirm your registration'), subtitle: t('Auth.SIGNUP_CONFIRM_SUBTITLE', 'Choose a password to finish creating your account.'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t('Auth.SIGNUP_CONFIRM_TITLE', 'Confirm your registration')] }) }), tokenMissing ? (_jsxs(_Fragment, { children: [_jsx(Alert, { severity: "error", sx: SIGNUP_CONFIRM_PAGE_ALERT_SX, children: t('Auth.PENDING_TOKEN_INVALID', 'This confirmation link is invalid or expired.') }), _jsxs(Stack, { direction: "row", spacing: 1, sx: { mt: 1 }, children: [_jsx(Button, { onClick: () => navigate('/signup'), variant: "contained", children: t('Auth.SIGNUP_REQUEST_NEW', 'Request a new invitation') }), _jsx(Button, { onClick: () => navigate('/login'), variant: "text", children: t('Auth.SIGNUP_GO_TO_LOGIN', 'Go to login') })] })] })) : (_jsxs(_Fragment, { children: [errorKey && (_jsx(Alert, { severity: "error", sx: SIGNUP_CONFIRM_PAGE_ALERT_SX, children: errorKey })), _jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { display: 'flex', flexDirection: 'column', gap: 2 }, children: [_jsx(TextField, { label: t('Auth.NEW_PASSWORD_LABEL', 'New password'), type: "password", required: true, fullWidth: true, value: password, onChange: (e) => setPassword(e.target.value), disabled: submitting, autoComplete: "new-password" }), _jsx(TextField, { label: t('Auth.PASSWORD_CONFIRM_LABEL', 'Confirm new password'), type: "password", required: true, fullWidth: true, value: confirmPassword, onChange: (e) => setConfirmPassword(e.target.value), disabled: submitting, autoComplete: "new-password" }), _jsx(Button, { type: "submit", variant: "contained", disabled: submitting, children: submitting
|
|
62
63
|
? t('Auth.SIGNUP_CONFIRM_SUBMITTING', 'Confirming…')
|
|
63
64
|
: t('Auth.SIGNUP_CONFIRM_SUBMIT', 'Confirm registration') })] }), _jsxs(Stack, { direction: "row", spacing: 1, sx: { mt: 3 }, children: [_jsx(Typography, { variant: "body2", children: t('Auth.SIGNUP_ALREADY_HAVE_ACCOUNT', 'Already have an account?') }), _jsx(Button, { onClick: () => navigate('/login'), variant: "text", size: "small", children: t('Auth.SIGNUP_GO_TO_LOGIN', 'Go to login') })] })] }))] }));
|
|
64
65
|
}
|
|
@@ -1,25 +1,59 @@
|
|
|
1
1
|
// src/utils/auth-errors.js
|
|
2
2
|
export function extractErrorInfo(error) {
|
|
3
|
-
var _a, _b, _c, _d;
|
|
3
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
4
4
|
const status = (_b = (_a = error.response) === null || _a === void 0 ? void 0 : _a.status) !== null && _b !== void 0 ? _b : null;
|
|
5
5
|
const data = (_d = (_c = error.response) === null || _c === void 0 ? void 0 : _c.data) !== null && _d !== void 0 ? _d : null;
|
|
6
6
|
if (!data) {
|
|
7
|
-
return {
|
|
7
|
+
return {
|
|
8
|
+
status,
|
|
9
|
+
code: null,
|
|
10
|
+
field: null,
|
|
11
|
+
i18nKey: null,
|
|
12
|
+
message: null,
|
|
13
|
+
raw: null,
|
|
14
|
+
};
|
|
8
15
|
}
|
|
9
16
|
// Allauth Headless structure often nests errors in "errors" array or "status" key
|
|
10
17
|
if (Array.isArray(data.errors) && data.errors.length > 0) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
const first = data.errors[0] || {};
|
|
19
|
+
return {
|
|
20
|
+
status,
|
|
21
|
+
code: (_e = first.code) !== null && _e !== void 0 ? _e : null,
|
|
22
|
+
field: (_g = (_f = first.field) !== null && _f !== void 0 ? _f : first.param) !== null && _g !== void 0 ? _g : null,
|
|
23
|
+
i18nKey: (_h = first.i18nKey) !== null && _h !== void 0 ? _h : null,
|
|
24
|
+
message: first.message,
|
|
25
|
+
raw: data,
|
|
26
|
+
};
|
|
14
27
|
}
|
|
15
28
|
if (typeof data.code === 'string') {
|
|
16
|
-
return {
|
|
29
|
+
return {
|
|
30
|
+
status,
|
|
31
|
+
code: data.code,
|
|
32
|
+
field: (_j = data.field) !== null && _j !== void 0 ? _j : null,
|
|
33
|
+
i18nKey: (_k = data.i18nKey) !== null && _k !== void 0 ? _k : null,
|
|
34
|
+
message: (_l = data.message) !== null && _l !== void 0 ? _l : data.detail,
|
|
35
|
+
raw: data,
|
|
36
|
+
};
|
|
17
37
|
}
|
|
18
38
|
// Fallback for generic Django errors
|
|
19
39
|
if (typeof data.detail === 'string') {
|
|
20
|
-
return {
|
|
40
|
+
return {
|
|
41
|
+
status,
|
|
42
|
+
code: null,
|
|
43
|
+
field: (_m = data.field) !== null && _m !== void 0 ? _m : null,
|
|
44
|
+
i18nKey: (_o = data.i18nKey) !== null && _o !== void 0 ? _o : null,
|
|
45
|
+
message: data.detail,
|
|
46
|
+
raw: data,
|
|
47
|
+
};
|
|
21
48
|
}
|
|
22
|
-
return {
|
|
49
|
+
return {
|
|
50
|
+
status,
|
|
51
|
+
code: null,
|
|
52
|
+
field: (_p = data.field) !== null && _p !== void 0 ? _p : null,
|
|
53
|
+
i18nKey: (_q = data.i18nKey) !== null && _q !== void 0 ? _q : null,
|
|
54
|
+
message: data.message,
|
|
55
|
+
raw: data,
|
|
56
|
+
};
|
|
23
57
|
}
|
|
24
58
|
export function normaliseApiError(error, defaultCode = 'Auth.GENERIC_ERROR') {
|
|
25
59
|
const info = extractErrorInfo(error);
|
|
@@ -27,6 +61,9 @@ export function normaliseApiError(error, defaultCode = 'Auth.GENERIC_ERROR') {
|
|
|
27
61
|
const message = info.message || code || defaultCode;
|
|
28
62
|
const err = new Error(message);
|
|
29
63
|
err.code = code;
|
|
64
|
+
err.field = info.field;
|
|
65
|
+
err.i18nKey = info.i18nKey;
|
|
66
|
+
err.backendMessage = info.message;
|
|
30
67
|
err.status = info.status;
|
|
31
68
|
err.raw = info.raw;
|
|
32
69
|
return err;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { extractErrorInfo } from './auth-errors';
|
|
2
|
+
export function resolveErrorText(i18n, err, defaultKey) {
|
|
3
|
+
// API helpers normally throw normalized errors. Keeping this small boundary
|
|
4
|
+
// adapter also makes the resolver safe for callers that pass an Axios error
|
|
5
|
+
// directly (for example, an app-level wrapper around a kit API call).
|
|
6
|
+
const info = (err === null || err === void 0 ? void 0 : err.response) ? extractErrorInfo(err) : null;
|
|
7
|
+
const resolvedErr = info
|
|
8
|
+
? Object.assign(Object.assign({}, err), { code: info.code || err.code, field: info.field, i18nKey: info.i18nKey, backendMessage: info.message }) : err;
|
|
9
|
+
const exists = (key) => Boolean(key) && i18n.exists(key);
|
|
10
|
+
if (!resolvedErr)
|
|
11
|
+
return i18n.t(defaultKey);
|
|
12
|
+
if (exists(resolvedErr.i18nKey))
|
|
13
|
+
return i18n.t(resolvedErr.i18nKey);
|
|
14
|
+
if (resolvedErr.field && resolvedErr.code) {
|
|
15
|
+
const fieldKey = `Auth.field.${resolvedErr.field}.${resolvedErr.code}`;
|
|
16
|
+
if (exists(fieldKey))
|
|
17
|
+
return i18n.t(fieldKey);
|
|
18
|
+
}
|
|
19
|
+
if (exists(resolvedErr.code))
|
|
20
|
+
return i18n.t(resolvedErr.code);
|
|
21
|
+
if (resolvedErr.backendMessage)
|
|
22
|
+
return resolvedErr.backendMessage;
|
|
23
|
+
return i18n.t(defaultKey);
|
|
24
|
+
}
|
package/package.json
CHANGED
package/src/auth/apiClient.jsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
|
+
import i18next from "i18next";
|
|
2
3
|
import { CSRF_URL } from "./authConfig";
|
|
3
4
|
import { requestReauth } from "./reauth";
|
|
4
5
|
|
|
@@ -8,6 +9,14 @@ const apiClient = axios.create({
|
|
|
8
9
|
xsrfHeaderName: "X-CSRFToken",
|
|
9
10
|
});
|
|
10
11
|
|
|
12
|
+
apiClient.interceptors.request.use((config) => {
|
|
13
|
+
const language = i18next?.isInitialized ? i18next.language : null;
|
|
14
|
+
if (language) {
|
|
15
|
+
config.headers = { ...config.headers, 'Accept-Language': language };
|
|
16
|
+
}
|
|
17
|
+
return config;
|
|
18
|
+
});
|
|
19
|
+
|
|
11
20
|
let redirectingToLogin = false;
|
|
12
21
|
|
|
13
22
|
function isBrowser() {
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
|
14
14
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|
15
15
|
import { useTranslation } from 'react-i18next';
|
|
16
|
+
import { resolveErrorText } from '../utils/resolveErrorText';
|
|
16
17
|
|
|
17
18
|
// Pfad ggf. anpassen je nach Struktur: von src/auth/components zu src/auth/authApi
|
|
18
19
|
import {
|
|
@@ -36,7 +37,7 @@ export const ACCESS_CODE_VALUE_FIELD_SX = {
|
|
|
36
37
|
};
|
|
37
38
|
|
|
38
39
|
export function AccessCodeManager() {
|
|
39
|
-
const { t } = useTranslation();
|
|
40
|
+
const { t, i18n } = useTranslation();
|
|
40
41
|
const [codes, setCodes] = useState([]);
|
|
41
42
|
const [loading, setLoading] = useState(true);
|
|
42
43
|
const [submitting, setSubmitting] = useState(false);
|
|
@@ -50,8 +51,7 @@ export function AccessCodeManager() {
|
|
|
50
51
|
|
|
51
52
|
// Helper that prefers backend error code if available
|
|
52
53
|
const setErrorFromErrorObject = (err, fallbackCode) => {
|
|
53
|
-
|
|
54
|
-
setErrorKey(backendCode || fallbackCode);
|
|
54
|
+
setErrorKey(resolveErrorText(i18n, err, fallbackCode));
|
|
55
55
|
};
|
|
56
56
|
|
|
57
57
|
// Load all access codes from backend
|
|
@@ -104,7 +104,7 @@ export function AccessCodeManager() {
|
|
|
104
104
|
|
|
105
105
|
const handleAddManual = async () => {
|
|
106
106
|
if (!manualCode.trim()) {
|
|
107
|
-
setErrorKey('Auth.SIGNUP_ACCESS_CODE_REQUIRED');
|
|
107
|
+
setErrorKey(t('Auth.SIGNUP_ACCESS_CODE_REQUIRED'));
|
|
108
108
|
return;
|
|
109
109
|
}
|
|
110
110
|
await handleCreateCode(manualCode.trim());
|
|
@@ -150,7 +150,7 @@ export function AccessCodeManager() {
|
|
|
150
150
|
<Box>
|
|
151
151
|
{errorKey && (
|
|
152
152
|
<Alert severity="error" sx={ACCESS_CODE_ALERT_SX}>
|
|
153
|
-
{
|
|
153
|
+
{errorKey}
|
|
154
154
|
</Alert>
|
|
155
155
|
)}
|
|
156
156
|
{successKey && (
|
|
@@ -8,11 +8,12 @@ import {
|
|
|
8
8
|
} from '@mui/material';
|
|
9
9
|
import { useTranslation } from 'react-i18next';
|
|
10
10
|
import { fetchAuthPolicy, updateAuthPolicy } from '../auth/authApi';
|
|
11
|
+
import { resolveErrorText } from '../utils/resolveErrorText';
|
|
11
12
|
|
|
12
13
|
export const ACCESS_CODE_SINGLE_USE_ALERT_SX = { mb: 2 };
|
|
13
14
|
|
|
14
15
|
export function AccessCodeSingleUseToggle({ canEdit = true, policy = null, onPolicyChange = null }) {
|
|
15
|
-
const { t } = useTranslation();
|
|
16
|
+
const { t, i18n } = useTranslation();
|
|
16
17
|
const [value, setValue] = useState(false);
|
|
17
18
|
const [busy, setBusy] = useState(false);
|
|
18
19
|
const [error, setError] = useState('');
|
|
@@ -57,7 +58,7 @@ export function AccessCodeSingleUseToggle({ canEdit = true, policy = null, onPol
|
|
|
57
58
|
setSuccess(t('Auth.ACCESS_CODE_SINGLE_USE_SAVE_SUCCESS', 'Access code policy saved.'));
|
|
58
59
|
} catch (err) {
|
|
59
60
|
setValue(previous);
|
|
60
|
-
setError(
|
|
61
|
+
setError(resolveErrorText(i18n, err, 'Auth.AUTH_POLICY_UPDATE_FAILED'));
|
|
61
62
|
} finally {
|
|
62
63
|
setBusy(false);
|
|
63
64
|
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from '@mui/material';
|
|
9
9
|
import { useTranslation } from 'react-i18next';
|
|
10
10
|
import { updateAuthPolicy } from '../auth/authApi';
|
|
11
|
+
import { resolveErrorText } from '../utils/resolveErrorText';
|
|
11
12
|
|
|
12
13
|
export const ALLOWED_EMAIL_DOMAINS_ALERT_SX = { mb: 2 };
|
|
13
14
|
export const ALLOWED_EMAIL_DOMAINS_BUTTON_SX = { mt: 2 };
|
|
@@ -18,7 +19,7 @@ export function AllowedEmailDomainsManager({
|
|
|
18
19
|
onPolicyChange,
|
|
19
20
|
canEdit = true,
|
|
20
21
|
}) {
|
|
21
|
-
const { t } = useTranslation();
|
|
22
|
+
const { t, i18n } = useTranslation();
|
|
22
23
|
const [domainsText, setDomainsText] = useState('');
|
|
23
24
|
const [busy, setBusy] = useState(false);
|
|
24
25
|
const [error, setError] = useState('');
|
|
@@ -44,7 +45,7 @@ export function AllowedEmailDomainsManager({
|
|
|
44
45
|
setSuccess(t('Auth.AUTH_POLICY_SAVE_SUCCESS', 'Authentication settings saved.'));
|
|
45
46
|
if (onPolicyChange) onPolicyChange(next);
|
|
46
47
|
} catch (err) {
|
|
47
|
-
setError(
|
|
48
|
+
setError(resolveErrorText(i18n, err, 'Auth.AUTH_POLICY_UPDATE_FAILED'));
|
|
48
49
|
} finally {
|
|
49
50
|
setBusy(false);
|
|
50
51
|
}
|
|
@@ -10,11 +10,12 @@ import {
|
|
|
10
10
|
} from '@mui/material';
|
|
11
11
|
import { useTranslation } from 'react-i18next';
|
|
12
12
|
import { fetchAuthPolicy, updateAuthPolicy } from '../auth/authApi';
|
|
13
|
+
import { resolveErrorText } from '../utils/resolveErrorText';
|
|
13
14
|
|
|
14
15
|
export const AUTH_FACTOR_REQUIREMENT_ALERT_SX = { mb: 2 };
|
|
15
16
|
|
|
16
17
|
export function AuthFactorRequirementCard({ canEdit = true, policy = null }) {
|
|
17
|
-
const { t } = useTranslation();
|
|
18
|
+
const { t, i18n } = useTranslation();
|
|
18
19
|
const [value, setValue] = useState('1');
|
|
19
20
|
const [busy, setBusy] = useState(false);
|
|
20
21
|
const [error, setError] = useState('');
|
|
@@ -56,7 +57,7 @@ export function AuthFactorRequirementCard({ canEdit = true, policy = null }) {
|
|
|
56
57
|
setSuccess(t('Auth.AUTH_FACTOR_SAVE_SUCCESS', 'Factor requirement saved.'));
|
|
57
58
|
} catch (err) {
|
|
58
59
|
setValue(previous);
|
|
59
|
-
setError(
|
|
60
|
+
setError(resolveErrorText(i18n, err, 'Auth.AUTH_POLICY_UPDATE_FAILED'));
|
|
60
61
|
} finally {
|
|
61
62
|
setBusy(false);
|
|
62
63
|
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from '@mui/material';
|
|
16
16
|
import { useTranslation } from 'react-i18next';
|
|
17
17
|
import { sendAdminInvite } from '../auth/authApi';
|
|
18
|
+
import { resolveErrorText } from '../utils/resolveErrorText';
|
|
18
19
|
|
|
19
20
|
export const BULK_INVITE_ACTION_SX = {
|
|
20
21
|
minWidth: 120,
|
|
@@ -51,7 +52,7 @@ export function BulkInviteCsvTab({
|
|
|
51
52
|
inviteFn = (email) => sendAdminInvite(email),
|
|
52
53
|
onCompleted,
|
|
53
54
|
}) {
|
|
54
|
-
const { t } = useTranslation();
|
|
55
|
+
const { t, i18n } = useTranslation();
|
|
55
56
|
const [emails, setEmails] = useState([]);
|
|
56
57
|
const [results, setResults] = useState({});
|
|
57
58
|
const [busy, setBusy] = useState(false);
|
|
@@ -106,7 +107,7 @@ export function BulkInviteCsvTab({
|
|
|
106
107
|
} catch (err) {
|
|
107
108
|
nextResults[email] = {
|
|
108
109
|
ok: false,
|
|
109
|
-
message:
|
|
110
|
+
message: resolveErrorText(i18n, err, 'Auth.INVITE_FAILED'),
|
|
110
111
|
};
|
|
111
112
|
}
|
|
112
113
|
setResults({ ...nextResults });
|