@micha.bigler/ui-core-micha 1.4.19 → 1.4.22
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/dist/auth/AuthContext.js +0 -1
- package/dist/auth/authApi.js +137 -378
- package/dist/components/MFAComponent.js +7 -7
- package/dist/components/MfaLoginComponent.js +6 -5
- package/dist/components/PasskeysComponent.js +5 -5
- package/dist/components/SecurityComponent.js +4 -3
- package/dist/components/SupportRecoveryRequestsTab.js +5 -5
- package/dist/components/UserInviteComponent.js +38 -0
- package/dist/components/UserListComponent.js +83 -0
- package/dist/i18n/authTranslations.js +25 -0
- package/dist/pages/AccountPage.js +67 -23
- package/dist/pages/LoginPage.js +6 -5
- package/dist/pages/PasswordInvitePage.js +3 -3
- package/dist/pages/SignUpPage.js +3 -3
- package/dist/utils/authService.js +53 -0
- package/dist/utils/errors.js +33 -0
- package/dist/utils/webauthn.js +44 -0
- package/package.json +1 -1
- package/src/auth/AuthContext.jsx +0 -1
- package/src/auth/authApi.jsx +143 -478
- package/src/components/MFAComponent.jsx +7 -7
- package/src/components/MfaLoginComponent.jsx +6 -5
- package/src/components/PasskeysComponent.jsx +5 -5
- package/src/components/SecurityComponent.jsx +4 -3
- package/src/components/SupportRecoveryRequestsTab.jsx +7 -5
- package/src/components/UserInviteComponent.jsx +69 -0
- package/src/components/UserListComponent.jsx +167 -0
- package/src/i18n/authTranslations.js +25 -0
- package/src/pages/AccountPage.jsx +140 -47
- package/src/pages/LoginPage.jsx +6 -5
- package/src/pages/PasswordInvitePage.jsx +3 -3
- package/src/pages/SignUpPage.jsx +3 -3
- package/src/utils/authService.js +68 -0
- package/src/utils/errors.js +39 -0
- package/src/utils/webauthn.js +51 -0
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|
19
19
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
|
20
20
|
import { useTranslation } from 'react-i18next';
|
|
21
|
-
import {
|
|
21
|
+
import { fetchAuthenticators, requestTotpKey, activateTotp, deactivateTotp, fetchRecoveryCodes, generateRecoveryCodes } from '../auth/authApi';
|
|
22
22
|
|
|
23
23
|
const MFAComponent = () => {
|
|
24
24
|
const { t } = useTranslation();
|
|
@@ -41,7 +41,7 @@ const MFAComponent = () => {
|
|
|
41
41
|
setLoading(true);
|
|
42
42
|
setErrorKey(null);
|
|
43
43
|
try {
|
|
44
|
-
const data = await
|
|
44
|
+
const data = await fetchAuthenticators();
|
|
45
45
|
setAuthenticators(Array.isArray(data) ? data : []);
|
|
46
46
|
} catch (err) {
|
|
47
47
|
// 404 / leer einfach schlucken
|
|
@@ -64,7 +64,7 @@ const MFAComponent = () => {
|
|
|
64
64
|
setIsSettingUp(true);
|
|
65
65
|
|
|
66
66
|
try {
|
|
67
|
-
const result = await
|
|
67
|
+
const result = await requestTotpKey();
|
|
68
68
|
|
|
69
69
|
if (result.exists) {
|
|
70
70
|
setIsSettingUp(false);
|
|
@@ -90,7 +90,7 @@ const MFAComponent = () => {
|
|
|
90
90
|
setSubmitting(true);
|
|
91
91
|
setErrorKey(null);
|
|
92
92
|
try {
|
|
93
|
-
await
|
|
93
|
+
await activateTotp(verifyCode);
|
|
94
94
|
setIsSettingUp(false);
|
|
95
95
|
setVerifyCode('');
|
|
96
96
|
setTotpData(null);
|
|
@@ -112,7 +112,7 @@ const MFAComponent = () => {
|
|
|
112
112
|
|
|
113
113
|
setErrorKey(null);
|
|
114
114
|
try {
|
|
115
|
-
await
|
|
115
|
+
await deactivateTotp();
|
|
116
116
|
await loadData();
|
|
117
117
|
} catch (err) {
|
|
118
118
|
// eslint-disable-next-line no-console
|
|
@@ -125,7 +125,7 @@ const MFAComponent = () => {
|
|
|
125
125
|
const handleShowRecoveryCodes = async () => {
|
|
126
126
|
setErrorKey(null);
|
|
127
127
|
try {
|
|
128
|
-
const data = await
|
|
128
|
+
const data = await fetchRecoveryCodes();
|
|
129
129
|
const codes = data.codes || data.unused_codes || [];
|
|
130
130
|
setRecoveryCodes(Array.isArray(codes) ? codes : []);
|
|
131
131
|
setShowRecovery(true);
|
|
@@ -139,7 +139,7 @@ const MFAComponent = () => {
|
|
|
139
139
|
const handleGenerateNewRecoveryCodes = async () => {
|
|
140
140
|
setErrorKey(null);
|
|
141
141
|
try {
|
|
142
|
-
const data = await
|
|
142
|
+
const data = await generateRecoveryCodes();
|
|
143
143
|
const codes = data.codes || data.unused_codes || [];
|
|
144
144
|
setRecoveryCodes(Array.isArray(codes) ? codes : []);
|
|
145
145
|
} catch (err) {
|
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
DialogActions,
|
|
15
15
|
} from '@mui/material';
|
|
16
16
|
import { useTranslation } from 'react-i18next';
|
|
17
|
-
import {
|
|
17
|
+
import { authenticateWithMFA, fetchCurrentUser, requestMfaSupportHelp } from '../auth/authApi';
|
|
18
|
+
import { loginWithPasskey } from '../utils/authService';
|
|
18
19
|
|
|
19
20
|
const MfaLoginComponent = ({ availableTypes, identifier, onSuccess, onCancel }) => {
|
|
20
21
|
const { t } = useTranslation();
|
|
@@ -41,9 +42,9 @@ const MfaLoginComponent = ({ availableTypes, identifier, onSuccess, onCancel })
|
|
|
41
42
|
const trimmed = code.trim();
|
|
42
43
|
const isRecovery = trimmed.length > 6;
|
|
43
44
|
|
|
44
|
-
await
|
|
45
|
+
await authenticateWithMFA({ code: trimmed });
|
|
45
46
|
|
|
46
|
-
const user = await
|
|
47
|
+
const user = await fetchCurrentUser();
|
|
47
48
|
|
|
48
49
|
onSuccess({
|
|
49
50
|
user,
|
|
@@ -61,7 +62,7 @@ const MfaLoginComponent = ({ availableTypes, identifier, onSuccess, onCancel })
|
|
|
61
62
|
setInfoKey(null);
|
|
62
63
|
setSubmitting(true);
|
|
63
64
|
try {
|
|
64
|
-
const { user } = await
|
|
65
|
+
const { user } = await loginWithPasskey();
|
|
65
66
|
onSuccess({ user, method: 'webauthn' });
|
|
66
67
|
} catch (err) {
|
|
67
68
|
setErrorKey(err.code || 'Auth.PASSKEY_FAILED');
|
|
@@ -81,7 +82,7 @@ const MfaLoginComponent = ({ availableTypes, identifier, onSuccess, onCancel })
|
|
|
81
82
|
setInfoKey(null);
|
|
82
83
|
setSubmitting(true);
|
|
83
84
|
try {
|
|
84
|
-
await
|
|
85
|
+
await requestMfaSupportHelp(identifier || '', helpMessage || '');
|
|
85
86
|
setHelpRequested(true);
|
|
86
87
|
setInfoKey('Auth.MFA_HELP_REQUESTED');
|
|
87
88
|
setHelpDialogOpen(false);
|
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
} from '@mui/material';
|
|
19
19
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|
20
20
|
import { useTranslation } from 'react-i18next';
|
|
21
|
-
import {
|
|
21
|
+
import { fetchPasskeys, registerPasskey, deletePasskey } from '../auth/authApi';
|
|
22
|
+
import { registerPasskey } from '../utils/authService';
|
|
22
23
|
import { FEATURES } from '../auth/authConfig';
|
|
23
24
|
|
|
24
25
|
const PasskeysComponent = () => {
|
|
@@ -41,10 +42,9 @@ const PasskeysComponent = () => {
|
|
|
41
42
|
const loadPasskeys = async () => {
|
|
42
43
|
setErrorKey(null);
|
|
43
44
|
try {
|
|
44
|
-
const data = await
|
|
45
|
+
const data = await fetchPasskeys();
|
|
45
46
|
setPasskeys(Array.isArray(data) ? data : []);
|
|
46
47
|
} catch (err) {
|
|
47
|
-
// authApi.fetchPasskeys wirft bereits normalisierte Errors mit .code
|
|
48
48
|
setErrorKey(err.code || 'Auth.PASSKEY_LIST_FAILED');
|
|
49
49
|
} finally {
|
|
50
50
|
setLoading(false);
|
|
@@ -74,7 +74,7 @@ const PasskeysComponent = () => {
|
|
|
74
74
|
const fallbackName =
|
|
75
75
|
name?.trim() || `Passkey on ${navigator.platform || 'this device'}`;
|
|
76
76
|
|
|
77
|
-
await
|
|
77
|
+
await registerPasskey(fallbackName);
|
|
78
78
|
setMessageKey('Auth.PASSKEY_CREATE_SUCCESS');
|
|
79
79
|
setName('');
|
|
80
80
|
|
|
@@ -94,7 +94,7 @@ const PasskeysComponent = () => {
|
|
|
94
94
|
|
|
95
95
|
setDeletingIds((prev) => new Set(prev).add(id));
|
|
96
96
|
try {
|
|
97
|
-
await
|
|
97
|
+
await deletePasskey(id);
|
|
98
98
|
setPasskeys((prev) => prev.filter((pk) => pk.id !== id));
|
|
99
99
|
setMessageKey('Auth.PASSKEY_DELETE_SUCCESS');
|
|
100
100
|
} catch (err) {
|
|
@@ -11,7 +11,8 @@ import PasswordChangeForm from './PasswordChangeForm';
|
|
|
11
11
|
import SocialLoginButtons from './SocialLoginButtons';
|
|
12
12
|
import PasskeysComponent from './PasskeysComponent';
|
|
13
13
|
import MFAComponent from './MFAComponent';
|
|
14
|
-
import {
|
|
14
|
+
import { changePassword } from '../auth/authApi';
|
|
15
|
+
import { startSocialLogin } from '../utils/authService';
|
|
15
16
|
|
|
16
17
|
const SecurityComponent = ({
|
|
17
18
|
fromRecovery = false,
|
|
@@ -26,7 +27,7 @@ const SecurityComponent = ({
|
|
|
26
27
|
setMessageKey(null);
|
|
27
28
|
setErrorKey(null);
|
|
28
29
|
try {
|
|
29
|
-
await
|
|
30
|
+
await startSocialLogin(provider);
|
|
30
31
|
// Redirect läuft über den Provider-Flow, hier kein extra Text nötig
|
|
31
32
|
} catch (err) {
|
|
32
33
|
setErrorKey(err.code || 'Auth.SOCIAL_LOGIN_FAILED');
|
|
@@ -37,7 +38,7 @@ const SecurityComponent = ({
|
|
|
37
38
|
setMessageKey(null);
|
|
38
39
|
setErrorKey(null);
|
|
39
40
|
try {
|
|
40
|
-
await
|
|
41
|
+
await changePassword(currentPassword, newPassword);
|
|
41
42
|
setMessageKey('Auth.RESET_PASSWORD_SUCCESS');
|
|
42
43
|
} catch (err) {
|
|
43
44
|
setErrorKey(err.code || 'Auth.PASSWORD_CHANGE_FAILED');
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
TextField,
|
|
22
22
|
} from '@mui/material';
|
|
23
23
|
import { useTranslation } from 'react-i18next';
|
|
24
|
-
import {
|
|
24
|
+
import { fetchRecoveryRequests, approveRecoveryRequest, rejectRecoveryRequest } from '../auth/authApi';
|
|
25
25
|
|
|
26
26
|
const SupportRecoveryRequestsTab = () => {
|
|
27
27
|
const { t } = useTranslation();
|
|
@@ -39,7 +39,7 @@ const SupportRecoveryRequestsTab = () => {
|
|
|
39
39
|
setLoading(true);
|
|
40
40
|
setErrorKey(null);
|
|
41
41
|
try {
|
|
42
|
-
const data = await
|
|
42
|
+
const data = await fetchRecoveryRequests(statusFilter);
|
|
43
43
|
setRequests(Array.isArray(data) ? data : []);
|
|
44
44
|
} catch (err) {
|
|
45
45
|
setErrorKey(err.code || 'Support.RECOVERY_REQUESTS_LOAD_FAILED');
|
|
@@ -69,7 +69,7 @@ const SupportRecoveryRequestsTab = () => {
|
|
|
69
69
|
if (!selectedRequest) return;
|
|
70
70
|
setErrorKey(null);
|
|
71
71
|
try {
|
|
72
|
-
await
|
|
72
|
+
await approveRecoveryRequest(selectedRequest.id, dialogNote);
|
|
73
73
|
await loadRequests();
|
|
74
74
|
closeDialog();
|
|
75
75
|
} catch (err) {
|
|
@@ -81,7 +81,7 @@ const SupportRecoveryRequestsTab = () => {
|
|
|
81
81
|
if (!selectedRequest) return;
|
|
82
82
|
setErrorKey(null);
|
|
83
83
|
try {
|
|
84
|
-
await
|
|
84
|
+
await rejectRecoveryRequest(selectedRequest.id, dialogNote);
|
|
85
85
|
await loadRequests();
|
|
86
86
|
closeDialog();
|
|
87
87
|
} catch (err) {
|
|
@@ -175,7 +175,9 @@ const SupportRecoveryRequestsTab = () => {
|
|
|
175
175
|
: '-'}
|
|
176
176
|
</TableCell>
|
|
177
177
|
<TableCell>{req.user_email || req.user}</TableCell>
|
|
178
|
-
<TableCell>
|
|
178
|
+
<TableCell>
|
|
179
|
+
{t(`Support.RECOVERY_STATUS_${req.status}`, req.status)}
|
|
180
|
+
</TableCell>
|
|
179
181
|
<TableCell>
|
|
180
182
|
<Button
|
|
181
183
|
variant="contained"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, TextField, Button, Typography, Alert, CircularProgress } from '@mui/material';
|
|
3
|
+
import { inviteUserByAdmin } from '../auth/authApi';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
|
|
6
|
+
export function UserInviteComponent({ apiUrl = '/api/users/' }) {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
const [inviteEmail, setInviteEmail] = useState('');
|
|
9
|
+
const [message, setMessage] = useState('');
|
|
10
|
+
const [error, setError] = useState('');
|
|
11
|
+
const [loading, setLoading] = useState(false);
|
|
12
|
+
|
|
13
|
+
const inviteUser = async () => {
|
|
14
|
+
setMessage('');
|
|
15
|
+
setError('');
|
|
16
|
+
if (!inviteEmail) return;
|
|
17
|
+
|
|
18
|
+
setLoading(true);
|
|
19
|
+
try {
|
|
20
|
+
// API Call via authApi
|
|
21
|
+
const data = await inviteUserByAdmin(inviteEmail, apiUrl);
|
|
22
|
+
|
|
23
|
+
setInviteEmail('');
|
|
24
|
+
setMessage(data.detail || t('Auth.INVITE_SENT_SUCCESS', 'Invitation sent.'));
|
|
25
|
+
} catch (err) {
|
|
26
|
+
// err.message enthält dank authApi bereits den normalisierten Text oder Code
|
|
27
|
+
console.error('Error inviting user:', err);
|
|
28
|
+
// Fallback Text, falls der Key nicht übersetzt ist
|
|
29
|
+
setError(t(err.code) || err.message || t('Auth.INVITE_FAILED'));
|
|
30
|
+
} finally {
|
|
31
|
+
setLoading(false);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Box sx={{ maxWidth: 600, mt: 2 }}>
|
|
37
|
+
<Typography variant="h6" gutterBottom>
|
|
38
|
+
{t('Auth.INVITE_TITLE', 'Invite a new user')}
|
|
39
|
+
</Typography>
|
|
40
|
+
|
|
41
|
+
{message && <Alert severity="success" sx={{ mb: 2 }}>{message}</Alert>}
|
|
42
|
+
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
|
43
|
+
|
|
44
|
+
<Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-start' }}>
|
|
45
|
+
<TextField
|
|
46
|
+
label={t('Auth.EMAIL_LABEL', 'Email address')}
|
|
47
|
+
type="email"
|
|
48
|
+
variant="outlined"
|
|
49
|
+
fullWidth
|
|
50
|
+
size="small"
|
|
51
|
+
value={inviteEmail}
|
|
52
|
+
onChange={(e) => setInviteEmail(e.target.value)}
|
|
53
|
+
disabled={loading}
|
|
54
|
+
onKeyPress={(e) => {
|
|
55
|
+
if (e.key === 'Enter') inviteUser();
|
|
56
|
+
}}
|
|
57
|
+
/>
|
|
58
|
+
<Button
|
|
59
|
+
variant="contained"
|
|
60
|
+
onClick={inviteUser}
|
|
61
|
+
disabled={loading || !inviteEmail}
|
|
62
|
+
sx={{ minWidth: 100, height: 40 }}
|
|
63
|
+
>
|
|
64
|
+
{loading ? <CircularProgress size={24} color="inherit" /> : t('Auth.INVITE_BUTTON', 'Invite')}
|
|
65
|
+
</Button>
|
|
66
|
+
</Box>
|
|
67
|
+
</Box>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Box, Typography, Table, TableBody, TableCell, TableContainer,
|
|
4
|
+
TableHead, TableRow, Paper, FormControl, InputLabel, Select,
|
|
5
|
+
MenuItem, Button, IconButton, Tooltip, CircularProgress, Alert
|
|
6
|
+
} from '@mui/material';
|
|
7
|
+
import DeleteIcon from '@mui/icons-material/Delete';
|
|
8
|
+
import { useTranslation } from 'react-i18next';
|
|
9
|
+
import { fetchUsersList, deleteUser, updateUserRole, updateUserSupportStatus } from '../auth/authApi'; // Nur noch authApi!
|
|
10
|
+
|
|
11
|
+
const DEFAULT_ROLES = ['none', 'student', 'teacher', 'admin'];
|
|
12
|
+
|
|
13
|
+
export function UserListComponent({
|
|
14
|
+
roles = DEFAULT_ROLES,
|
|
15
|
+
apiUrl = '/api/users/',
|
|
16
|
+
currentUser
|
|
17
|
+
}) {
|
|
18
|
+
const { t } = useTranslation();
|
|
19
|
+
const [users, setUsers] = useState([]);
|
|
20
|
+
const [loading, setLoading] = useState(true);
|
|
21
|
+
const [error, setError] = useState(null);
|
|
22
|
+
|
|
23
|
+
const loadUsers = async () => {
|
|
24
|
+
setLoading(true);
|
|
25
|
+
setError(null);
|
|
26
|
+
try {
|
|
27
|
+
const data = await fetchUsersList(apiUrl);
|
|
28
|
+
setUsers(data);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
// err.code ist dank normaliseApiError verfügbar
|
|
31
|
+
setError(err.code || 'Auth.USER_LIST_FAILED');
|
|
32
|
+
} finally {
|
|
33
|
+
setLoading(false);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
loadUsers();
|
|
39
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
40
|
+
}, [apiUrl]);
|
|
41
|
+
|
|
42
|
+
const handleDelete = async (userId) => {
|
|
43
|
+
if (!window.confirm(t('UserList.DELETE_CONFIRM', 'Are you sure you want to delete this user?'))) return;
|
|
44
|
+
try {
|
|
45
|
+
await deleteUser(userId, apiUrl);
|
|
46
|
+
// Optimistic update oder reload:
|
|
47
|
+
setUsers(prev => prev.filter(u => u.id !== userId));
|
|
48
|
+
} catch (err) {
|
|
49
|
+
alert(t(err.code || 'Auth.USER_DELETE_FAILED'));
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleChangeRole = async (userId, newRole) => {
|
|
54
|
+
try {
|
|
55
|
+
await updateUserRole(userId, newRole, apiUrl);
|
|
56
|
+
// Reload list to ensure consistency or update local state
|
|
57
|
+
loadUsers();
|
|
58
|
+
} catch (err) {
|
|
59
|
+
alert(t(err.code || 'Auth.USER_ROLE_UPDATE_FAILED'));
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleToggleSupporter = async (userId, newValue) => {
|
|
64
|
+
try {
|
|
65
|
+
await updateUserSupportStatus(userId, newValue, apiUrl);
|
|
66
|
+
loadUsers();
|
|
67
|
+
} catch (err) {
|
|
68
|
+
alert(t(err.code || 'Auth.USER_UPDATE_FAILED'));
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const canEdit = (targetUser) => {
|
|
73
|
+
if (!currentUser) return false;
|
|
74
|
+
if (currentUser.is_superuser) return true;
|
|
75
|
+
|
|
76
|
+
const myRole = currentUser.role || 'none';
|
|
77
|
+
const targetRole = targetUser.role || 'none';
|
|
78
|
+
|
|
79
|
+
if (myRole === 'admin') return true;
|
|
80
|
+
|
|
81
|
+
// Beispiel Logik: Lehrer dürfen Schüler bearbeiten
|
|
82
|
+
if (myRole === 'teacher') {
|
|
83
|
+
if (targetUser.id === currentUser.id) return false;
|
|
84
|
+
if (['teacher', 'admin', 'supervisor'].includes(targetRole)) return false;
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Box>
|
|
92
|
+
<Typography variant="h6" gutterBottom>{t('UserList.TITLE', 'All Users')}</Typography>
|
|
93
|
+
|
|
94
|
+
{error && <Alert severity="error" sx={{ mb: 2 }}>{t(error)}</Alert>}
|
|
95
|
+
|
|
96
|
+
{loading ? (
|
|
97
|
+
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
|
|
98
|
+
<CircularProgress />
|
|
99
|
+
</Box>
|
|
100
|
+
) : (
|
|
101
|
+
<TableContainer component={Paper}>
|
|
102
|
+
<Table size="small">
|
|
103
|
+
<TableHead>
|
|
104
|
+
<TableRow>
|
|
105
|
+
<TableCell>{t('Auth.EMAIL_LABEL', 'Email')}</TableCell>
|
|
106
|
+
<TableCell>{t('Profile.NAME_LABEL', 'Name')}</TableCell>
|
|
107
|
+
<TableCell>{t('UserList.ROLE', 'Role')}</TableCell>
|
|
108
|
+
<TableCell>{t('UserList.SUPPORTER', 'Support Agent')}</TableCell>
|
|
109
|
+
<TableCell>{t('Common.ACTIONS', 'Actions')}</TableCell>
|
|
110
|
+
</TableRow>
|
|
111
|
+
</TableHead>
|
|
112
|
+
<TableBody>
|
|
113
|
+
{users.map((u) => (
|
|
114
|
+
<TableRow key={u.id}>
|
|
115
|
+
<TableCell>{u.email}</TableCell>
|
|
116
|
+
<TableCell>
|
|
117
|
+
{u.first_name || u.last_name ? `${u.first_name} ${u.last_name}` : u.username}
|
|
118
|
+
</TableCell>
|
|
119
|
+
<TableCell>
|
|
120
|
+
<FormControl size="small" fullWidth disabled={!canEdit(u)}>
|
|
121
|
+
<Select
|
|
122
|
+
value={u.role || 'none'}
|
|
123
|
+
onChange={(e) => handleChangeRole(u.id, e.target.value)}
|
|
124
|
+
variant="standard"
|
|
125
|
+
disableUnderline
|
|
126
|
+
>
|
|
127
|
+
{roles.map(r => <MenuItem key={r} value={r}>{r}</MenuItem>)}
|
|
128
|
+
</Select>
|
|
129
|
+
</FormControl>
|
|
130
|
+
</TableCell>
|
|
131
|
+
<TableCell>
|
|
132
|
+
<Button
|
|
133
|
+
size="small"
|
|
134
|
+
variant={u.is_support_agent ? 'contained' : 'outlined'}
|
|
135
|
+
color={u.is_support_agent ? 'primary' : 'inherit'}
|
|
136
|
+
onClick={() => handleToggleSupporter(u.id, !u.is_support_agent)}
|
|
137
|
+
disabled={!canEdit(u)}
|
|
138
|
+
sx={{ textTransform: 'none' }}
|
|
139
|
+
>
|
|
140
|
+
{u.is_support_agent ? t('Common.YES') : t('Common.NO')}
|
|
141
|
+
</Button>
|
|
142
|
+
</TableCell>
|
|
143
|
+
<TableCell>
|
|
144
|
+
<Tooltip title={t('Common.DELETE')}>
|
|
145
|
+
<span>
|
|
146
|
+
<IconButton onClick={() => handleDelete(u.id)} color="error" disabled={!canEdit(u)}>
|
|
147
|
+
<DeleteIcon />
|
|
148
|
+
</IconButton>
|
|
149
|
+
</span>
|
|
150
|
+
</Tooltip>
|
|
151
|
+
</TableCell>
|
|
152
|
+
</TableRow>
|
|
153
|
+
))}
|
|
154
|
+
{users.length === 0 && (
|
|
155
|
+
<TableRow>
|
|
156
|
+
<TableCell colSpan={5} align="center">
|
|
157
|
+
{t('UserList.NO_USERS', 'No users found.')}
|
|
158
|
+
</TableCell>
|
|
159
|
+
</TableRow>
|
|
160
|
+
)}
|
|
161
|
+
</TableBody>
|
|
162
|
+
</Table>
|
|
163
|
+
</TableContainer>
|
|
164
|
+
)}
|
|
165
|
+
</Box>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -978,4 +978,29 @@ export const authTranslations = {
|
|
|
978
978
|
"fr": "Si un compte existe avec cette adresse e-mail, votre demande a été transmise au support.",
|
|
979
979
|
"en": "If an account with this email exists, your request has been forwarded to support."
|
|
980
980
|
},
|
|
981
|
+
"Support.RECOVERY_STATUS_pending": {
|
|
982
|
+
"de": "Offen",
|
|
983
|
+
"fr": "En attente",
|
|
984
|
+
"en": "Pending"
|
|
985
|
+
},
|
|
986
|
+
"Support.RECOVERY_STATUS_approved": {
|
|
987
|
+
"de": "Genehmigt",
|
|
988
|
+
"fr": "Approuvée",
|
|
989
|
+
"en": "Approved"
|
|
990
|
+
},
|
|
991
|
+
"Support.RECOVERY_STATUS_rejected": {
|
|
992
|
+
"de": "Abgelehnt",
|
|
993
|
+
"fr": "Refusée",
|
|
994
|
+
"en": "Rejected"
|
|
995
|
+
},
|
|
996
|
+
"Support.RECOVERY_STATUS_completed": {
|
|
997
|
+
"de": "Abgeschlossen",
|
|
998
|
+
"fr": "Terminée",
|
|
999
|
+
"en": "Completed"
|
|
1000
|
+
},
|
|
1001
|
+
"Support.RECOVERY_STATUS_expired": {
|
|
1002
|
+
"de": "Abgelaufen",
|
|
1003
|
+
"fr": "Expirée",
|
|
1004
|
+
"en": "Expired"
|
|
1005
|
+
}
|
|
981
1006
|
};
|