@micha.bigler/ui-core-micha 3.8.0 → 3.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/WORK_ORDERS.md +2 -0
  3. package/dist/auth/apiClient.js +8 -0
  4. package/dist/components/AccessCodeManager.js +5 -5
  5. package/dist/components/AccessCodeSingleUseToggle.js +3 -2
  6. package/dist/components/AllowedEmailDomainsManager.js +3 -2
  7. package/dist/components/AuthFactorRequirementCard.js +3 -2
  8. package/dist/components/BulkInviteCsvTab.js +3 -2
  9. package/dist/components/MFAComponent.js +9 -8
  10. package/dist/components/MfaLoginComponent.js +7 -6
  11. package/dist/components/PasskeysComponent.js +7 -6
  12. package/dist/components/ProfileComponent.js +4 -3
  13. package/dist/components/QrSignupManager.js +4 -3
  14. package/dist/components/QrSignupValidityManager.js +3 -2
  15. package/dist/components/RegistrationMethodsManager.js +3 -2
  16. package/dist/components/SecurityComponent.js +5 -4
  17. package/dist/components/SupportRecoveryRequestsTab.js +8 -7
  18. package/dist/components/UserInviteComponent.js +3 -2
  19. package/dist/components/UserListComponent.js +14 -7
  20. package/dist/i18n/authTranslations.js +420 -0
  21. package/dist/pages/AccountPage.js +3 -2
  22. package/dist/pages/LoginPage.js +10 -9
  23. package/dist/pages/PasswordChangePage.js +4 -3
  24. package/dist/pages/PasswordInvitePage.js +7 -6
  25. package/dist/pages/PasswordResetRequestPage.js +5 -4
  26. package/dist/pages/SignUpPage.js +7 -6
  27. package/dist/pages/SignupConfirmPage.js +7 -6
  28. package/dist/utils/auth-errors.js +45 -8
  29. package/dist/utils/resolveErrorText.js +24 -0
  30. package/package.json +1 -1
  31. package/src/auth/apiClient.jsx +9 -0
  32. package/src/components/AccessCodeManager.jsx +5 -5
  33. package/src/components/AccessCodeSingleUseToggle.jsx +3 -2
  34. package/src/components/AllowedEmailDomainsManager.jsx +3 -2
  35. package/src/components/AuthFactorRequirementCard.jsx +3 -2
  36. package/src/components/BulkInviteCsvTab.jsx +3 -2
  37. package/src/components/MFAComponent.jsx +9 -8
  38. package/src/components/MfaLoginComponent.jsx +7 -6
  39. package/src/components/PasskeysComponent.jsx +7 -6
  40. package/src/components/ProfileComponent.jsx +4 -3
  41. package/src/components/QrSignupManager.jsx +4 -3
  42. package/src/components/QrSignupValidityManager.jsx +3 -2
  43. package/src/components/RegistrationMethodsManager.jsx +3 -2
  44. package/src/components/SecurityComponent.jsx +5 -4
  45. package/src/components/SupportRecoveryRequestsTab.jsx +9 -8
  46. package/src/components/UserInviteComponent.jsx +3 -2
  47. package/src/components/UserListComponent.jsx +14 -7
  48. package/src/i18n/authTranslations.ts +421 -0
  49. package/src/pages/AccountPage.jsx +3 -4
  50. package/src/pages/LoginPage.jsx +10 -9
  51. package/src/pages/PasswordChangePage.jsx +4 -3
  52. package/src/pages/PasswordInvitePage.jsx +7 -6
  53. package/src/pages/PasswordResetRequestPage.jsx +5 -4
  54. package/src/pages/SignUpPage.jsx +7 -6
  55. package/src/pages/SignupConfirmPage.jsx +7 -6
  56. package/src/utils/auth-errors.js +44 -7
  57. package/src/utils/resolveErrorText.js +28 -0
  58. package/tests/AccountPage.test.jsx +27 -20
  59. package/tests/PasswordInvitePage.test.jsx +28 -21
  60. package/tests/QrSignupManager.registrationContext.test.jsx +5 -1
  61. package/tests/SignUpPage.test.jsx +14 -3
  62. package/tests/UserListComponent.test.jsx +100 -16
  63. package/tests/apiClientLanguage.test.js +33 -0
  64. package/tests/authErrors.test.js +31 -0
  65. package/tests/errorCallSiteGuard.test.js +32 -0
  66. package/tests/loginPageErrorResolution.test.jsx +102 -0
  67. package/tests/resolveErrorText.test.js +72 -0
  68. package/work-orders/UCM-AUTH-10.md +490 -0
  69. package/work-orders/UCM-AUTH-11.md +185 -0
@@ -18,6 +18,7 @@ import { useTranslation } from 'react-i18next';
18
18
  // WICHTIG: Importiere den Context, um die bereits geladenen Daten zu nutzen
19
19
  import { AuthContext } from '../auth/AuthContext';
20
20
  import { fetchCookieStatement, fetchPrivacyStatement } from '../auth/authApi';
21
+ import { resolveErrorText } from '../utils/resolveErrorText';
21
22
 
22
23
  export function ProfileComponent({
23
24
  onSubmit,
@@ -29,7 +30,7 @@ export function ProfileComponent({
29
30
  privacyStatementText = null,
30
31
  cookieStatementText = null,
31
32
  }) {
32
- const { t } = useTranslation();
33
+ const { t, i18n } = useTranslation();
33
34
 
34
35
  // WICHTIG: Wir holen den User direkt aus dem globalen State
35
36
  // Das verhindert den doppelten Request und den ReferenceError
@@ -140,7 +141,7 @@ export function ProfileComponent({
140
141
  } catch (err) {
141
142
  // eslint-disable-next-line no-console
142
143
  console.error("Profile update error:", err);
143
- setErrorKey(err.code || 'Auth.PROFILE_UPDATE_FAILED');
144
+ setErrorKey(resolveErrorText(i18n, err, 'Auth.PROFILE_UPDATE_FAILED'));
144
145
  } finally {
145
146
  setSaving(false);
146
147
  }
@@ -175,7 +176,7 @@ export function ProfileComponent({
175
176
  >
176
177
  {errorKey && (
177
178
  <Alert severity="error">
178
- {t(errorKey)}
179
+ {errorKey}
179
180
  </Alert>
180
181
  )}
181
182
  {successKey && (
@@ -10,6 +10,7 @@ import {
10
10
  import { useTranslation } from 'react-i18next';
11
11
  import { QRCodeSVG } from 'qrcode.react';
12
12
  import { createSignupQr } from '../auth/authApi';
13
+ import { resolveErrorText } from '../utils/resolveErrorText';
13
14
 
14
15
  export const QR_SIGNUP_MANAGER_ALERT_SX = { mb: 2 };
15
16
 
@@ -47,7 +48,7 @@ export function QrSignupManager({
47
48
  registrationContext = null,
48
49
  defaultMaxRedemptions = DEFAULT_MAX_REDEMPTIONS,
49
50
  }) {
50
- const { t } = useTranslation();
51
+ const { t, i18n } = useTranslation();
51
52
  const qrWrapperRef = useRef(null);
52
53
  const [busy, setBusy] = useState(false);
53
54
  const [error, setError] = useState('');
@@ -112,7 +113,7 @@ export function QrSignupManager({
112
113
  hasGeneratedRef.current = true;
113
114
  setSuccess(t('Auth.SIGNUP_QR_CREATE_SUCCESS', 'New QR signup link created.'));
114
115
  } catch (err) {
115
- setError(t(err?.code || 'Auth.SIGNUP_QR_CREATE_FAILED', 'Could not create signup QR.'));
116
+ setError(resolveErrorText(i18n, err, 'Auth.SIGNUP_QR_CREATE_FAILED'));
116
117
  } finally {
117
118
  setBusy(false);
118
119
  }
@@ -158,7 +159,7 @@ export function QrSignupManager({
158
159
  setSuccess(t('Auth.SIGNUP_QR_CREATE_SUCCESS', 'New QR signup link created.'));
159
160
  } catch (err) {
160
161
  if (!active) return;
161
- setError(t(err?.code || 'Auth.SIGNUP_QR_CREATE_FAILED', 'Could not create signup QR.'));
162
+ setError(resolveErrorText(i18n, err, 'Auth.SIGNUP_QR_CREATE_FAILED'));
162
163
  } finally {
163
164
  if (active) {
164
165
  setBusy(false);
@@ -9,6 +9,7 @@ import {
9
9
  } from '@mui/material';
10
10
  import { useTranslation } from 'react-i18next';
11
11
  import { updateAuthPolicy } from '../auth/authApi';
12
+ import { resolveErrorText } from '../utils/resolveErrorText';
12
13
 
13
14
  export const QR_SIGNUP_VALIDITY_ALERT_SX = { mb: 2 };
14
15
  export const QR_SIGNUP_VALIDITY_FIELD_SX = { flex: 1 };
@@ -30,7 +31,7 @@ export function QrSignupValidityManager({
30
31
  onPolicyChange,
31
32
  canEdit = true,
32
33
  }) {
33
- const { t } = useTranslation();
34
+ const { t, i18n } = useTranslation();
34
35
  const [currentExpiryDays, setCurrentExpiryDays] = useState(String(expiryDays || DEFAULT_EXPIRY_DAYS));
35
36
  const [busy, setBusy] = useState(false);
36
37
  const [error, setError] = useState('');
@@ -53,7 +54,7 @@ export function QrSignupValidityManager({
53
54
  setSuccess(t('Auth.AUTH_POLICY_SAVE_SUCCESS', 'Authentication settings saved.'));
54
55
  if (onPolicyChange) onPolicyChange(next);
55
56
  } catch (err) {
56
- setError(t(err?.code || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save authentication settings.'));
57
+ setError(resolveErrorText(i18n, err, 'Auth.AUTH_POLICY_UPDATE_FAILED'));
57
58
  } finally {
58
59
  setBusy(false);
59
60
  }
@@ -10,6 +10,7 @@ import {
10
10
  } from '@mui/material';
11
11
  import { useTranslation } from 'react-i18next';
12
12
  import { updateAuthPolicy } from '../auth/authApi';
13
+ import { resolveErrorText } from '../utils/resolveErrorText';
13
14
 
14
15
  export const REGISTRATION_METHODS_ALERT_SX = { mb: 2 };
15
16
  export const REGISTRATION_METHODS_INFO_SX = { mt: 2 };
@@ -31,7 +32,7 @@ export function RegistrationMethodsManager({
31
32
  onPolicyChange,
32
33
  canEdit = true,
33
34
  }) {
34
- const { t } = useTranslation();
35
+ const { t, i18n } = useTranslation();
35
36
  const [policyState, setPolicyState] = useState(EMPTY_POLICY);
36
37
  const [busyField, setBusyField] = useState('');
37
38
  const [saveError, setSaveError] = useState('');
@@ -57,7 +58,7 @@ export function RegistrationMethodsManager({
57
58
  if (onPolicyChange) onPolicyChange(next);
58
59
  } catch (err) {
59
60
  setPolicyState((prev) => ({ ...prev, [field]: previous }));
60
- setSaveError(t(err?.code || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save authentication settings.'));
61
+ setSaveError(resolveErrorText(i18n, err, 'Auth.AUTH_POLICY_UPDATE_FAILED'));
61
62
  } finally {
62
63
  setBusyField('');
63
64
  }
@@ -14,6 +14,7 @@ import { MFAComponent } from './MFAComponent';
14
14
  import { changePassword } from '../auth/authApi';
15
15
  import { startSocialLogin } from '../utils/authService';
16
16
  import { AuthContext } from '../auth/AuthContext';
17
+ import { resolveErrorText } from '../utils/resolveErrorText';
17
18
 
18
19
  export const SECURITY_ALERT_SX = { mb: 2 };
19
20
  export const SECURITY_DIVIDER_SX = { my: 3 };
@@ -22,7 +23,7 @@ export function SecurityComponent({
22
23
  fromRecovery = false,
23
24
  fromWeakLogin = false,
24
25
  }) {
25
- const { t } = useTranslation();
26
+ const { t, i18n } = useTranslation();
26
27
  const { authMethods } = useContext(AuthContext);
27
28
 
28
29
  const [messageKey, setMessageKey] = useState(null);
@@ -60,7 +61,7 @@ export function SecurityComponent({
60
61
  callbackUrl: `${window.location.origin}/account?tab=security`,
61
62
  });
62
63
  } catch (err) {
63
- setErrorKey(err.code || 'Auth.SOCIAL_LOGIN_FAILED');
64
+ setErrorKey(resolveErrorText(i18n, err, 'Auth.SOCIAL_LOGIN_FAILED'));
64
65
  }
65
66
  };
66
67
 
@@ -71,7 +72,7 @@ export function SecurityComponent({
71
72
  await changePassword(currentPassword, newPassword);
72
73
  setMessageKey('Auth.RESET_PASSWORD_SUCCESS');
73
74
  } catch (err) {
74
- setErrorKey(err.code || 'Auth.PASSWORD_CHANGE_FAILED');
75
+ setErrorKey(resolveErrorText(i18n, err, 'Auth.PASSWORD_CHANGE_FAILED'));
75
76
  }
76
77
  };
77
78
 
@@ -96,7 +97,7 @@ export function SecurityComponent({
96
97
  )}
97
98
  {errorKey && (
98
99
  <Alert severity="error" sx={SECURITY_ALERT_SX}>
99
- {t(errorKey)}
100
+ {errorKey}
100
101
  </Alert>
101
102
  )}
102
103
 
@@ -24,6 +24,7 @@ import {
24
24
  } from '@mui/material';
25
25
  import CloseIcon from '@mui/icons-material/Close';
26
26
  import { useTranslation } from 'react-i18next';
27
+ import { resolveErrorText } from '../utils/resolveErrorText';
27
28
  import { AuthContext } from '../auth/AuthContext';
28
29
  import {
29
30
  approveRecoveryRequest,
@@ -63,7 +64,7 @@ export function SupportRecoveryRequestsTab() {
63
64
  const data = await fetchRecoveryRequests(statusFilter);
64
65
  setRequests(Array.isArray(data) ? data : []);
65
66
  } catch (err) {
66
- setErrorKey(err.code || 'Support.RECOVERY_REQUESTS_LOAD_FAILED');
67
+ setErrorKey(resolveErrorText(i18n, err, 'Support.RECOVERY_REQUESTS_LOAD_FAILED'));
67
68
  } finally {
68
69
  setLoading(false);
69
70
  }
@@ -88,7 +89,7 @@ export function SupportRecoveryRequestsTab() {
88
89
  });
89
90
  setAllUsers(list);
90
91
  } catch (err) {
91
- setAgentErrorKey(err.code || 'Auth.USER_LIST_FAILED');
92
+ setAgentErrorKey(resolveErrorText(i18n, err, 'Auth.USER_LIST_FAILED'));
92
93
  } finally {
93
94
  setAgentLoading(false);
94
95
  }
@@ -124,7 +125,7 @@ export function SupportRecoveryRequestsTab() {
124
125
  await loadRequests();
125
126
  closeDialog();
126
127
  } catch (err) {
127
- setErrorKey(err.code || 'Support.RECOVERY_REQUEST_APPROVE_FAILED');
128
+ setErrorKey(resolveErrorText(i18n, err, 'Support.RECOVERY_REQUEST_APPROVE_FAILED'));
128
129
  }
129
130
  };
130
131
 
@@ -136,7 +137,7 @@ export function SupportRecoveryRequestsTab() {
136
137
  await loadRequests();
137
138
  closeDialog();
138
139
  } catch (err) {
139
- setErrorKey(err.code || 'Support.RECOVERY_REQUEST_REJECT_FAILED');
140
+ setErrorKey(resolveErrorText(i18n, err, 'Support.RECOVERY_REQUEST_REJECT_FAILED'));
140
141
  }
141
142
  };
142
143
 
@@ -167,7 +168,7 @@ export function SupportRecoveryRequestsTab() {
167
168
  setSelectedAgentCandidate(null);
168
169
  await loadUsers();
169
170
  } catch (err) {
170
- setAgentErrorKey(err.code || 'Auth.USER_SUPPORT_UPDATE_FAILED');
171
+ setAgentErrorKey(resolveErrorText(i18n, err, 'Auth.USER_SUPPORT_UPDATE_FAILED'));
171
172
  } finally {
172
173
  setAgentActionUserId(null);
173
174
  }
@@ -185,7 +186,7 @@ export function SupportRecoveryRequestsTab() {
185
186
  }
186
187
  await loadUsers();
187
188
  } catch (err) {
188
- setAgentErrorKey(err.code || 'Auth.USER_SUPPORT_UPDATE_FAILED');
189
+ setAgentErrorKey(resolveErrorText(i18n, err, 'Auth.USER_SUPPORT_UPDATE_FAILED'));
189
190
  } finally {
190
191
  setAgentActionUserId(null);
191
192
  }
@@ -216,7 +217,7 @@ export function SupportRecoveryRequestsTab() {
216
217
 
217
218
  {agentErrorKey && (
218
219
  <Alert severity="error" sx={SUPPORT_RECOVERY_ALERT_SX}>
219
- {t(agentErrorKey)}
220
+ {agentErrorKey}
220
221
  </Alert>
221
222
  )}
222
223
 
@@ -310,7 +311,7 @@ export function SupportRecoveryRequestsTab() {
310
311
 
311
312
  {errorKey && (
312
313
  <Alert severity="error" sx={SUPPORT_RECOVERY_ALERT_SX}>
313
- {t(errorKey)}
314
+ {errorKey}
314
315
  </Alert>
315
316
  )}
316
317
 
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
2
2
  import { Box, TextField, Button, Typography, Alert, CircularProgress } from '@mui/material';
3
3
  import { sendAdminInvite } from '../auth/authApi';
4
4
  import { useTranslation } from 'react-i18next';
5
+ import { resolveErrorText } from '../utils/resolveErrorText';
5
6
 
6
7
  export const USER_INVITE_ACTION_SX = {
7
8
  minWidth: 120,
@@ -11,7 +12,7 @@ export const USER_INVITE_ACTION_SX = {
11
12
  export const USER_INVITE_ALERT_SX = { mb: 2 };
12
13
 
13
14
  export function UserInviteComponent() { // FIX: Removed apiUrl prop
14
- const { t } = useTranslation();
15
+ const { t, i18n } = useTranslation();
15
16
  const [inviteEmail, setInviteEmail] = useState('');
16
17
  const [message, setMessage] = useState('');
17
18
  const [error, setError] = useState('');
@@ -31,7 +32,7 @@ export function UserInviteComponent() { // FIX: Removed apiUrl prop
31
32
  // err.message contains normalized text or code from authApi
32
33
  // eslint-disable-next-line no-console
33
34
  console.error('Error inviting user:', err);
34
- setError(t(err.code) || err.message || t('Auth.INVITE_FAILED'));
35
+ setError(resolveErrorText(i18n, err, 'Auth.INVITE_FAILED'));
35
36
  } finally {
36
37
  setLoading(false);
37
38
  }
@@ -24,6 +24,7 @@ import CancelIcon from '@mui/icons-material/Cancel';
24
24
  import DeleteIcon from '@mui/icons-material/Delete';
25
25
  import { useTranslation } from 'react-i18next';
26
26
  import { fetchUsersList, deleteUser, updateUserRole } from '../auth/authApi';
27
+ import { resolveErrorText } from '../utils/resolveErrorText';
27
28
 
28
29
  const DEFAULT_ROLES = ['none', 'student', 'teacher', 'admin'];
29
30
 
@@ -49,7 +50,7 @@ export function UserListComponent({
49
50
  canDeleteUser = null,
50
51
  onDeleteUser = null,
51
52
  }) {
52
- const { t } = useTranslation();
53
+ const { t, i18n } = useTranslation();
53
54
  const [users, setUsers] = useState([]);
54
55
  const [loading, setLoading] = useState(true);
55
56
  const [error, setError] = useState(null);
@@ -75,7 +76,7 @@ export function UserListComponent({
75
76
  });
76
77
  setUsers(list);
77
78
  } catch (err) {
78
- setError(err.code || 'Auth.USER_LIST_FAILED');
79
+ setError(resolveErrorText(i18n, err, 'Auth.USER_LIST_FAILED'));
79
80
  } finally {
80
81
  setLoading(false);
81
82
  }
@@ -99,7 +100,7 @@ export function UserListComponent({
99
100
  }
100
101
  setUsers(prev => prev.filter(u => u.id !== userId));
101
102
  } catch (err) {
102
- alert(t(err.code || 'Auth.USER_DELETE_FAILED'));
103
+ alert(resolveErrorText(i18n, err, 'Auth.USER_DELETE_FAILED'));
103
104
  }
104
105
  };
105
106
 
@@ -120,7 +121,7 @@ export function UserListComponent({
120
121
  await loadUsers();
121
122
  alert(t('UserList.ROLE_UPDATE_SUCCESS', 'Role updated.'));
122
123
  } catch (err) {
123
- alert(t(err.code || 'Auth.USER_ROLE_UPDATE_FAILED'));
124
+ alert(resolveErrorText(i18n, err, 'Auth.USER_ROLE_UPDATE_FAILED'));
124
125
  }
125
126
  };
126
127
 
@@ -154,6 +155,11 @@ export function UserListComponent({
154
155
  return canEdit(targetUser);
155
156
  };
156
157
 
158
+ const isOwnRow = (targetUser) => {
159
+ if (currentUser?.id == null || targetUser?.id == null) return false;
160
+ return String(targetUser.id) === String(currentUser.id);
161
+ };
162
+
157
163
  const listContext = useMemo(() => ({
158
164
  currentUser,
159
165
  extraContext,
@@ -276,7 +282,7 @@ export function UserListComponent({
276
282
  });
277
283
  } catch (err) {
278
284
  // eslint-disable-next-line no-alert
279
- alert(err?.message || t('Common.OPERATION_FAILED', 'Operation failed.'));
285
+ alert(resolveErrorText(i18n, err, 'Common.OPERATION_FAILED'));
280
286
  } finally {
281
287
  setRowActionLoading((prev) => ({ ...prev, [actionId]: false }));
282
288
  }
@@ -434,7 +440,7 @@ export function UserListComponent({
434
440
  );
435
441
  })}
436
442
 
437
- {showDeleteAction && (
443
+ {showDeleteAction && !isOwnRow(row) && (
438
444
  <Tooltip title={t('Common.DELETE', 'Delete')}>
439
445
  <span>
440
446
  <Button
@@ -472,6 +478,7 @@ export function UserListComponent({
472
478
  loadUsers,
473
479
  canEdit,
474
480
  canDelete,
481
+ isOwnRow,
475
482
  ]);
476
483
 
477
484
  const sortedUsers = useMemo(() => {
@@ -514,7 +521,7 @@ export function UserListComponent({
514
521
  />
515
522
  </Box>
516
523
 
517
- {error && <Alert severity="error" sx={USER_LIST_ALERT_SX}>{t(error)}</Alert>}
524
+ {error && <Alert severity="error" sx={USER_LIST_ALERT_SX}>{error}</Alert>}
518
525
 
519
526
  {loading && (
520
527
  <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>