@micha.bigler/ui-core-micha 1.4.11 → 1.4.13
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/authApi.js +4 -5
- package/dist/components/MfaLoginComponent.js +12 -3
- package/dist/components/SupportRecoveryRequestsTab.js +34 -21
- package/dist/i18n/authTranslations.js +205 -0
- package/package.json +1 -1
- package/src/auth/authApi.jsx +4 -4
- package/src/components/MfaLoginComponent.jsx +66 -2
- package/src/components/SupportRecoveryRequestsTab.jsx +93 -72
- package/src/i18n/authTranslations.js +212 -1
package/dist/auth/authApi.js
CHANGED
|
@@ -609,13 +609,12 @@ export async function fetchRecoveryRequests(status = 'pending') {
|
|
|
609
609
|
});
|
|
610
610
|
return res.data;
|
|
611
611
|
}
|
|
612
|
-
export async function approveRecoveryRequest(id,
|
|
613
|
-
const res = await axios.post(`/api/support/recovery-requests/${id}/approve/`, {
|
|
614
|
-
{ withCredentials: true });
|
|
612
|
+
export async function approveRecoveryRequest(id, supportNote) {
|
|
613
|
+
const res = await axios.post(`/api/support/recovery-requests/${id}/approve/`, { support_note: supportNote || '' }, { withCredentials: true });
|
|
615
614
|
return res.data;
|
|
616
615
|
}
|
|
617
|
-
export async function rejectRecoveryRequest(id) {
|
|
618
|
-
const res = await axios.post(`/api/support/recovery-requests/${id}/reject/`, {}, { withCredentials: true });
|
|
616
|
+
export async function rejectRecoveryRequest(id, supportNote) {
|
|
617
|
+
const res = await axios.post(`/api/support/recovery-requests/${id}/reject/`, { support_note: supportNote || '' }, { withCredentials: true });
|
|
619
618
|
return res.data;
|
|
620
619
|
}
|
|
621
620
|
export async function loginWithRecoveryPassword(email, password, token) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// src/auth/components/MfaLoginComponent.jsx
|
|
3
3
|
import React, { useState } from 'react';
|
|
4
|
-
import { Box, Typography, TextField, Button, Stack, Alert, Divider, } from '@mui/material';
|
|
4
|
+
import { Box, Typography, TextField, Button, Stack, Alert, Divider, Dialog, DialogTitle, DialogContent, DialogActions, } from '@mui/material';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import { authApi } from '../auth/authApi';
|
|
7
7
|
const MfaLoginComponent = ({ availableTypes, identifier, onSuccess, onCancel }) => {
|
|
@@ -11,6 +11,8 @@ const MfaLoginComponent = ({ availableTypes, identifier, onSuccess, onCancel })
|
|
|
11
11
|
const [errorKey, setErrorKey] = useState(null);
|
|
12
12
|
const [infoKey, setInfoKey] = useState(null);
|
|
13
13
|
const [helpRequested, setHelpRequested] = useState(false);
|
|
14
|
+
const [helpDialogOpen, setHelpDialogOpen] = useState(false);
|
|
15
|
+
const [helpMessage, setHelpMessage] = useState('');
|
|
14
16
|
const types = Array.isArray(availableTypes) ? availableTypes : [];
|
|
15
17
|
const supportsTotpOrRecovery = types.includes('totp') || types.includes('recovery_codes');
|
|
16
18
|
const supportsWebauthn = types.includes('webauthn');
|
|
@@ -51,14 +53,21 @@ const MfaLoginComponent = ({ availableTypes, identifier, onSuccess, onCancel })
|
|
|
51
53
|
setSubmitting(false);
|
|
52
54
|
}
|
|
53
55
|
};
|
|
56
|
+
const openHelpDialog = () => {
|
|
57
|
+
setErrorKey(null);
|
|
58
|
+
setInfoKey(null);
|
|
59
|
+
setHelpDialogOpen(true);
|
|
60
|
+
};
|
|
54
61
|
const handleNeedHelp = async () => {
|
|
55
62
|
setErrorKey(null);
|
|
56
63
|
setInfoKey(null);
|
|
57
64
|
setSubmitting(true);
|
|
58
65
|
try {
|
|
59
|
-
await authApi.requestMfaSupportHelp(identifier || '');
|
|
66
|
+
await authApi.requestMfaSupportHelp(identifier || '', helpMessage || '');
|
|
60
67
|
setHelpRequested(true);
|
|
61
68
|
setInfoKey('Auth.MFA_HELP_REQUESTED');
|
|
69
|
+
setHelpDialogOpen(false);
|
|
70
|
+
setHelpMessage('');
|
|
62
71
|
}
|
|
63
72
|
catch (err) {
|
|
64
73
|
setErrorKey(err.code || 'Auth.MFA_HELP_REQUEST_FAILED');
|
|
@@ -67,6 +76,6 @@ const MfaLoginComponent = ({ availableTypes, identifier, onSuccess, onCancel })
|
|
|
67
76
|
setSubmitting(false);
|
|
68
77
|
}
|
|
69
78
|
};
|
|
70
|
-
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "body2", sx: { mb: 2 }, children: t('Auth.MFA_SUBTITLE', 'Please confirm your login using one of the available methods.') }), errorKey && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(errorKey) })), infoKey && (_jsx(Alert, { severity: "info", sx: { mb: 2 }, children: t(infoKey) })), _jsxs(Stack, { spacing: 2, children: [supportsWebauthn && (_jsx(Button, { variant: "contained", fullWidth: true, onClick: handlePasskey, disabled: submitting || helpRequested, children: t('Auth.LOGIN_USE_PASSKEY_BUTTON', 'Use passkey / security key') })), _jsx(Divider, { sx: { my: 2 }, children: t('Auth.LOGIN_OR') }), supportsTotpOrRecovery && (_jsxs(Box, { component: "form", onSubmit: handleSubmitCode, children: [_jsx(TextField, { label: t('Auth.MFA_CODE_LABEL', 'Authenticator code (or recovery code)'), value: code, onChange: (e) => setCode(e.target.value), fullWidth: true, disabled: submitting || helpRequested, autoComplete: "one-time-code", sx: { mb: 2 } }), _jsx(Button, { type: "submit", variant: "contained", fullWidth: true, disabled: submitting || !code.trim() || helpRequested, children: t('Auth.MFA_VERIFY', 'Verify') })] })), _jsx(Divider, { sx: { my: 2 }, children: t('Auth.LOGIN_OR') }), _jsxs(Stack, { direction: "row", spacing: 1, sx: { mt: 1 }, children: [_jsx(Button, { fullWidth: true, size: "small", variant: "outlined", onClick: onCancel, disabled: submitting, children: t('Auth.MFA_BACK_TO_LOGIN', 'Back to login') }), _jsx(Button, { fullWidth: true, size: "small", variant: "outlined", color: "secondary", onClick:
|
|
79
|
+
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "body2", sx: { mb: 2 }, children: t('Auth.MFA_SUBTITLE', 'Please confirm your login using one of the available methods.') }), errorKey && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(errorKey) })), infoKey && (_jsx(Alert, { severity: "info", sx: { mb: 2 }, children: t(infoKey) })), _jsxs(Stack, { spacing: 2, children: [supportsWebauthn && (_jsx(Button, { variant: "contained", fullWidth: true, onClick: handlePasskey, disabled: submitting || helpRequested, children: t('Auth.LOGIN_USE_PASSKEY_BUTTON', 'Use passkey / security key') })), _jsx(Divider, { sx: { my: 2 }, children: t('Auth.LOGIN_OR') }), supportsTotpOrRecovery && (_jsxs(Box, { component: "form", onSubmit: handleSubmitCode, children: [_jsx(TextField, { label: t('Auth.MFA_CODE_LABEL', 'Authenticator code (or recovery code)'), value: code, onChange: (e) => setCode(e.target.value), fullWidth: true, disabled: submitting || helpRequested, autoComplete: "one-time-code", sx: { mb: 2 } }), _jsx(Button, { type: "submit", variant: "contained", fullWidth: true, disabled: submitting || !code.trim() || helpRequested, children: t('Auth.MFA_VERIFY', 'Verify') })] })), _jsx(Divider, { sx: { my: 2 }, children: t('Auth.LOGIN_OR') }), _jsxs(Stack, { direction: "row", spacing: 1, sx: { mt: 1 }, children: [_jsx(Button, { fullWidth: true, size: "small", variant: "outlined", onClick: onCancel, disabled: submitting, children: t('Auth.MFA_BACK_TO_LOGIN', 'Back to login') }), _jsx(Button, { fullWidth: true, size: "small", variant: "outlined", color: "secondary", onClick: openHelpDialog, disabled: submitting || helpRequested, children: t('Auth.MFA_NEED_HELP', "I can't use any of these methods") })] })] }), _jsxs(Dialog, { open: helpDialogOpen, onClose: () => setHelpDialogOpen(false), fullWidth: true, maxWidth: "sm", children: [_jsx(DialogTitle, { children: t('Auth.MFA_HELP_DIALOG_TITLE', 'Need help with sign-in') }), _jsxs(DialogContent, { dividers: true, children: [_jsx(Typography, { variant: "body2", sx: { mb: 2 }, children: t('Auth.MFA_HELP_DIALOG_DESCRIPTION', 'Describe briefly why you cannot use the available methods. A support person will review your request.') }), _jsx(TextField, { label: t('Auth.MFA_HELP_MESSAGE_LABEL', 'Your message to support'), helperText: t('Auth.MFA_HELP_MESSAGE_HELP', 'Optional, but helps support verify your identity.'), multiline: true, minRows: 3, fullWidth: true, value: helpMessage, onChange: (e) => setHelpMessage(e.target.value), disabled: submitting })] }), _jsxs(DialogActions, { children: [_jsx(Button, { onClick: () => setHelpDialogOpen(false), disabled: submitting, children: t('Common.CANCEL', 'Cancel') }), _jsx(Button, { onClick: handleNeedHelp, disabled: submitting, variant: "contained", children: t('Auth.MFA_HELP_SUBMIT', 'Send request') })] })] })] }));
|
|
71
80
|
};
|
|
72
81
|
export default MfaLoginComponent;
|
|
@@ -9,10 +9,10 @@ const SupportRecoveryRequestsTab = () => {
|
|
|
9
9
|
const [requests, setRequests] = useState([]);
|
|
10
10
|
const [loading, setLoading] = useState(true);
|
|
11
11
|
const [errorKey, setErrorKey] = useState(null);
|
|
12
|
-
const [statusFilter, setStatusFilter] = useState('pending');
|
|
13
|
-
const [
|
|
14
|
-
const [
|
|
15
|
-
const [
|
|
12
|
+
const [statusFilter, setStatusFilter] = useState('pending');
|
|
13
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
14
|
+
const [dialogNote, setDialogNote] = useState('');
|
|
15
|
+
const [selectedRequest, setSelectedRequest] = useState(null);
|
|
16
16
|
const loadRequests = async () => {
|
|
17
17
|
setLoading(true);
|
|
18
18
|
setErrorKey(null);
|
|
@@ -31,34 +31,37 @@ const SupportRecoveryRequestsTab = () => {
|
|
|
31
31
|
loadRequests();
|
|
32
32
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
33
33
|
}, [statusFilter]);
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
const openDialog = (req) => {
|
|
35
|
+
setSelectedRequest(req);
|
|
36
|
+
setDialogNote('');
|
|
37
|
+
setDialogOpen(true);
|
|
38
38
|
};
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
const closeDialog = () => {
|
|
40
|
+
setDialogOpen(false);
|
|
41
|
+
setSelectedRequest(null);
|
|
42
|
+
setDialogNote('');
|
|
43
43
|
};
|
|
44
|
-
const
|
|
45
|
-
if (!
|
|
44
|
+
const handleApprove = async () => {
|
|
45
|
+
if (!selectedRequest)
|
|
46
46
|
return;
|
|
47
47
|
setErrorKey(null);
|
|
48
48
|
try {
|
|
49
|
-
await authApi.approveRecoveryRequest(
|
|
49
|
+
await authApi.approveRecoveryRequest(selectedRequest.id, dialogNote);
|
|
50
50
|
await loadRequests();
|
|
51
|
-
|
|
51
|
+
closeDialog();
|
|
52
52
|
}
|
|
53
53
|
catch (err) {
|
|
54
54
|
setErrorKey(err.code || 'Support.RECOVERY_REQUEST_APPROVE_FAILED');
|
|
55
55
|
}
|
|
56
56
|
};
|
|
57
|
-
const handleReject = async (
|
|
57
|
+
const handleReject = async () => {
|
|
58
|
+
if (!selectedRequest)
|
|
59
|
+
return;
|
|
58
60
|
setErrorKey(null);
|
|
59
61
|
try {
|
|
60
|
-
await authApi.rejectRecoveryRequest(id);
|
|
62
|
+
await authApi.rejectRecoveryRequest(selectedRequest.id, dialogNote);
|
|
61
63
|
await loadRequests();
|
|
64
|
+
closeDialog();
|
|
62
65
|
}
|
|
63
66
|
catch (err) {
|
|
64
67
|
setErrorKey(err.code || 'Support.RECOVERY_REQUEST_REJECT_FAILED');
|
|
@@ -67,8 +70,18 @@ const SupportRecoveryRequestsTab = () => {
|
|
|
67
70
|
if (loading) {
|
|
68
71
|
return (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', mt: 4 }, children: _jsx(CircularProgress, {}) }));
|
|
69
72
|
}
|
|
70
|
-
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Support.RECOVERY_REQUESTS_TITLE', 'Account recovery requests') }), _jsx(Typography, { variant: "body2", sx: { mb: 2 }, children: t('Support.RECOVERY_REQUESTS_DESCRIPTION', 'Users who cannot complete MFA can request support. You can
|
|
73
|
+
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Support.RECOVERY_REQUESTS_TITLE', 'Account recovery requests') }), _jsx(Typography, { variant: "body2", sx: { mb: 2 }, children: t('Support.RECOVERY_REQUESTS_DESCRIPTION', 'Users who cannot complete MFA can request support. You can review their request and approve or reject it.') }), errorKey && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(errorKey) })), _jsxs(Stack, { direction: "row", spacing: 2, sx: { mb: 2 }, children: [_jsx(Button, { variant: statusFilter === 'pending' ? 'contained' : 'outlined', size: "small", onClick: () => setStatusFilter('pending'), children: t('Support.RECOVERY_FILTER_PENDING', 'Open') }), _jsx(Button, { variant: statusFilter === 'approved' ? 'contained' : 'outlined', size: "small", onClick: () => setStatusFilter('approved'), children: t('Support.RECOVERY_FILTER_APPROVED', 'Approved') }), _jsx(Button, { variant: statusFilter === 'rejected' ? 'contained' : 'outlined', size: "small", onClick: () => setStatusFilter('rejected'), children: t('Support.RECOVERY_FILTER_REJECTED', 'Rejected') })] }), requests.length === 0 ? (_jsx(Typography, { variant: "body2", children: t('Support.RECOVERY_REQUESTS_EMPTY', 'No recovery requests for this filter.') })) : (_jsx(TableContainer, { component: Paper, children: _jsxs(Table, { size: "small", children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { children: t('Support.RECOVERY_COL_CREATED', 'Created') }), _jsx(TableCell, { children: t('Support.RECOVERY_COL_USER', 'User') }), _jsx(TableCell, { children: t('Support.RECOVERY_COL_STATUS', 'Status') }), _jsx(TableCell, { children: t('Support.RECOVERY_COL_ACTIONS', 'Actions') })] }) }), _jsx(TableBody, { children: requests.map((req) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: req.created_at
|
|
71
74
|
? new Date(req.created_at).toLocaleString()
|
|
72
|
-
: '-' }), _jsx(TableCell, { children: req.user_email || req.user }), _jsx(TableCell, { children: req.status }), _jsx(TableCell, { children:
|
|
75
|
+
: '-' }), _jsx(TableCell, { children: req.user_email || req.user }), _jsx(TableCell, { children: req.status }), _jsx(TableCell, { children: _jsx(Button, { variant: "contained", size: "small", onClick: () => openDialog(req), disabled: req.status !== 'pending', children: t('Support.RECOVERY_ACTION_REVIEW', 'Review') }) })] }, req.id))) })] }) })), _jsxs(Dialog, { open: dialogOpen, onClose: closeDialog, fullWidth: true, maxWidth: "sm", children: [_jsx(DialogTitle, { children: t('Support.RECOVERY_REVIEW_DIALOG_TITLE', 'Review recovery request') }), _jsxs(DialogContent, { dividers: true, children: [selectedRequest && (_jsxs(Box, { sx: { mb: 2 }, children: [_jsxs(Typography, { variant: "body2", sx: { mb: 1 }, children: [_jsxs("strong", { children: [t('Support.RECOVERY_REVIEW_USER', 'User'), ":"] }), ' ', selectedRequest.user_email || selectedRequest.user] }), _jsxs(Typography, { variant: "body2", sx: { mb: 1 }, children: [_jsxs("strong", { children: [t('Support.RECOVERY_REVIEW_CREATED', 'Requested at'), ":"] }), ' ', selectedRequest.created_at
|
|
76
|
+
? new Date(selectedRequest.created_at).toLocaleString()
|
|
77
|
+
: '-'] }), _jsx(Typography, { variant: "body2", sx: { mb: 1 }, children: _jsxs("strong", { children: [t('Support.RECOVERY_REVIEW_MESSAGE', 'User’s explanation'), ":"] }) }), _jsx(Box, { sx: {
|
|
78
|
+
border: 1,
|
|
79
|
+
borderColor: 'divider',
|
|
80
|
+
borderRadius: 1,
|
|
81
|
+
p: 1,
|
|
82
|
+
mb: 2,
|
|
83
|
+
whiteSpace: 'pre-wrap',
|
|
84
|
+
fontSize: '0.875rem',
|
|
85
|
+
}, children: selectedRequest.message || t('Support.RECOVERY_NO_MESSAGE', 'No message provided.') })] })), _jsx(TextField, { label: t('Support.RECOVERY_NOTE_LABEL', 'Reason for your decision'), helperText: t('Support.RECOVERY_NOTE_HELP', 'Explain briefly why you approve or reject this request.'), multiline: true, minRows: 3, fullWidth: true, value: dialogNote, onChange: (e) => setDialogNote(e.target.value) })] }), _jsxs(DialogActions, { children: [_jsx(Button, { onClick: closeDialog, children: t('Common.CANCEL', 'Cancel') }), _jsx(Button, { onClick: handleReject, color: "error", disabled: !dialogNote.trim(), children: t('Support.RECOVERY_REJECT_SUBMIT', 'Reject request') }), _jsx(Button, { onClick: handleApprove, variant: "contained", disabled: !dialogNote.trim(), children: t('Support.RECOVERY_APPROVE_SUBMIT', 'Approve and send link') })] })] })] }));
|
|
73
86
|
};
|
|
74
|
-
export default
|
|
87
|
+
export default SupportRecove;
|
|
@@ -855,6 +855,211 @@ export const authTranslations = {
|
|
|
855
855
|
"de": "Link senden",
|
|
856
856
|
"fr": "Envoyer le lien",
|
|
857
857
|
"en": "Send link"
|
|
858
|
+
},
|
|
859
|
+
"Auth.MFA_HELP_DIALOG_TITLE": {
|
|
860
|
+
"de": "Hilfe bei der Anmeldung",
|
|
861
|
+
"fr": "Aide pour la connexion",
|
|
862
|
+
"en": "Need help with sign-in"
|
|
863
|
+
},
|
|
864
|
+
"Auth.MFA_HELP_DIALOG_DESCRIPTION": {
|
|
865
|
+
"de": "Beschreiben Sie kurz, warum Sie keine der verfügbaren Methoden verwenden können. Eine Supportperson wird Ihre Anfrage prüfen.",
|
|
866
|
+
"fr": "Décrivez brièvement pourquoi vous ne pouvez utiliser aucune des méthodes disponibles. Un membre du support examinera votre demande.",
|
|
867
|
+
"en": "Briefly describe why you cannot use any of the available methods. A support person will review your request."
|
|
868
|
+
},
|
|
869
|
+
"Auth.MFA_HELP_MESSAGE_LABEL": {
|
|
870
|
+
"de": "Ihre Nachricht an den Support",
|
|
871
|
+
"fr": "Votre message au support",
|
|
872
|
+
"en": "Your message to support"
|
|
873
|
+
},
|
|
874
|
+
"Auth.MFA_HELP_MESSAGE_HELP": {
|
|
875
|
+
"de": "Optional, hilft dem Support bei der Überprüfung Ihrer Identität.",
|
|
876
|
+
"fr": "Facultatif, mais aide le support à vérifier votre identité.",
|
|
877
|
+
"en": "Optional, but helps support verify your identity."
|
|
878
|
+
},
|
|
879
|
+
"Auth.MFA_HELP_SUBMIT": {
|
|
880
|
+
"de": "Anfrage senden",
|
|
881
|
+
"fr": "Envoyer la demande",
|
|
882
|
+
"en": "Send request"
|
|
883
|
+
},
|
|
884
|
+
"Support.RECOVERY_NOTE_LABEL": {
|
|
885
|
+
"de": "Begründung für diese Entscheidung",
|
|
886
|
+
"fr": "Raison de cette décision",
|
|
887
|
+
"en": "Reason for this decision"
|
|
888
|
+
},
|
|
889
|
+
"Support.RECOVERY_NOTE_HELP": {
|
|
890
|
+
"de": "Beschreiben Sie kurz, warum Sie diese Anfrage genehmigen oder ablehnen.",
|
|
891
|
+
"fr": "Décrivez brièvement pourquoi vous approuvez ou rejetez cette demande.",
|
|
892
|
+
"en": "Briefly describe why you are approving or rejecting this request."
|
|
893
|
+
},
|
|
894
|
+
"Support.RECOVERY_REJECT_DIALOG_TITLE": {
|
|
895
|
+
"de": "Konto-Wiederherstellung ablehnen",
|
|
896
|
+
"fr": "Refuser la récupération de compte",
|
|
897
|
+
"en": "Reject account recovery"
|
|
898
|
+
},
|
|
899
|
+
"Support.RECOVERY_REJECT_CONFIRM_QUESTION": {
|
|
900
|
+
"de": "Sind Sie sicher, dass Sie diese Wiederherstellungsanfrage ablehnen möchten?",
|
|
901
|
+
"fr": "Êtes-vous sûr de vouloir refuser cette demande de récupération ?",
|
|
902
|
+
"en": "Are you sure you want to reject this recovery request?"
|
|
903
|
+
},
|
|
904
|
+
"Support.RECOVERY_REJECT_SUBMIT": {
|
|
905
|
+
"de": "Anfrage ablehnen",
|
|
906
|
+
"fr": "Refuser la demande",
|
|
907
|
+
"en": "Reject request"
|
|
908
|
+
},
|
|
909
|
+
"Support.RECOVERY_REQUESTS_TITLE": {
|
|
910
|
+
"de": "Anfragen zur Kontowiederherstellung",
|
|
911
|
+
"fr": "Demandes de récupération de compte",
|
|
912
|
+
"en": "Account recovery requests"
|
|
913
|
+
},
|
|
914
|
+
"Support.RECOVERY_REQUESTS_DESCRIPTION": {
|
|
915
|
+
"de": "Benutzer, die die MFA nicht abschliessen können, können hier eine Anfrage stellen. Sie können die Anfrage prüfen und anschliessend akzeptieren oder ablehnen.",
|
|
916
|
+
"fr": "Les utilisateurs qui ne peuvent pas terminer la MFA peuvent envoyer ici une demande. Vous pouvez examiner la demande puis l’accepter ou la refuser.",
|
|
917
|
+
"en": "Users who cannot complete MFA can submit a request here. You can review the request and then approve or reject it."
|
|
918
|
+
},
|
|
919
|
+
"Support.RECOVERY_REQUESTS_LOAD_FAILED": {
|
|
920
|
+
"de": "Die Anfragen zur Kontowiederherstellung konnten nicht geladen werden.",
|
|
921
|
+
"fr": "Impossible de charger les demandes de récupération de compte.",
|
|
922
|
+
"en": "Failed to load account recovery requests."
|
|
923
|
+
},
|
|
924
|
+
"Support.RECOVERY_FILTER_PENDING": {
|
|
925
|
+
"de": "Offen",
|
|
926
|
+
"fr": "Ouvertes",
|
|
927
|
+
"en": "Open"
|
|
928
|
+
},
|
|
929
|
+
"Support.RECOVERY_FILTER_APPROVED": {
|
|
930
|
+
"de": "Akzeptiert",
|
|
931
|
+
"fr": "Acceptées",
|
|
932
|
+
"en": "Approved"
|
|
933
|
+
},
|
|
934
|
+
"Support.RECOVERY_FILTER_REJECTED": {
|
|
935
|
+
"de": "Abgelehnt",
|
|
936
|
+
"fr": "Refusées",
|
|
937
|
+
"en": "Rejected"
|
|
938
|
+
},
|
|
939
|
+
"Support.RECOVERY_REQUESTS_EMPTY": {
|
|
940
|
+
"de": "Für diesen Filter liegen keine Anfragen vor.",
|
|
941
|
+
"fr": "Aucune demande pour ce filtre.",
|
|
942
|
+
"en": "No recovery requests for this filter."
|
|
943
|
+
},
|
|
944
|
+
"Support.RECOVERY_COL_CREATED": {
|
|
945
|
+
"de": "Erstellt",
|
|
946
|
+
"fr": "Créée le",
|
|
947
|
+
"en": "Created"
|
|
948
|
+
},
|
|
949
|
+
"Support.RECOVERY_COL_USER": {
|
|
950
|
+
"de": "Benutzer",
|
|
951
|
+
"fr": "Utilisateur",
|
|
952
|
+
"en": "User"
|
|
953
|
+
},
|
|
954
|
+
"Support.RECOVERY_COL_STATUS": {
|
|
955
|
+
"de": "Status",
|
|
956
|
+
"fr": "Statut",
|
|
957
|
+
"en": "Status"
|
|
958
|
+
},
|
|
959
|
+
"Support.RECOVERY_COL_ACTIONS": {
|
|
960
|
+
"de": "Aktionen",
|
|
961
|
+
"fr": "Actions",
|
|
962
|
+
"en": "Actions"
|
|
963
|
+
},
|
|
964
|
+
"Support.RECOVERY_ACTION_REVIEW": {
|
|
965
|
+
"de": "Prüfen",
|
|
966
|
+
"fr": "Examiner",
|
|
967
|
+
"en": "Review"
|
|
968
|
+
},
|
|
969
|
+
"Support.RECOVERY_REVIEW_DIALOG_TITLE": {
|
|
970
|
+
"de": "Anfrage zur Kontowiederherstellung prüfen",
|
|
971
|
+
"fr": "Examiner la demande de récupération de compte",
|
|
972
|
+
"en": "Review recovery request"
|
|
973
|
+
},
|
|
974
|
+
"Support.RECOVERY_REVIEW_USER": {
|
|
975
|
+
"de": "Benutzer",
|
|
976
|
+
"fr": "Utilisateur",
|
|
977
|
+
"en": "User"
|
|
978
|
+
},
|
|
979
|
+
"Support.RECOVERY_REVIEW_CREATED": {
|
|
980
|
+
"de": "Angefragt am",
|
|
981
|
+
"fr": "Demandée le",
|
|
982
|
+
"en": "Requested at"
|
|
983
|
+
},
|
|
984
|
+
"Support.RECOVERY_REVIEW_MESSAGE": {
|
|
985
|
+
"de": "Begründung des Benutzers",
|
|
986
|
+
"fr": "Explication de l’utilisateur",
|
|
987
|
+
"en": "User’s explanation"
|
|
988
|
+
},
|
|
989
|
+
"Support.RECOVERY_NO_MESSAGE": {
|
|
990
|
+
"de": "Keine Begründung angegeben.",
|
|
991
|
+
"fr": "Aucune explication fournie.",
|
|
992
|
+
"en": "No message provided."
|
|
993
|
+
},
|
|
994
|
+
"Support.RECOVERY_NOTE_LABEL": {
|
|
995
|
+
"de": "Begründung für Ihre Entscheidung",
|
|
996
|
+
"fr": "Raison de votre décision",
|
|
997
|
+
"en": "Reason for your decision"
|
|
998
|
+
},
|
|
999
|
+
"Support.RECOVERY_NOTE_HELP": {
|
|
1000
|
+
"de": "Beschreiben Sie kurz, warum Sie diese Anfrage akzeptieren oder ablehnen.",
|
|
1001
|
+
"fr": "Décrivez brièvement pourquoi vous acceptez ou refusez cette demande.",
|
|
1002
|
+
"en": "Briefly explain why you approve or reject this request."
|
|
1003
|
+
},
|
|
1004
|
+
"Support.RECOVERY_REJECT_SUBMIT": {
|
|
1005
|
+
"de": "Anfrage ablehnen",
|
|
1006
|
+
"fr": "Refuser la demande",
|
|
1007
|
+
"en": "Reject request"
|
|
1008
|
+
},
|
|
1009
|
+
"Support.RECOVERY_APPROVE_SUBMIT": {
|
|
1010
|
+
"de": "Akzeptieren und Link senden",
|
|
1011
|
+
"fr": "Accepter et envoyer le lien",
|
|
1012
|
+
"en": "Approve and send link"
|
|
1013
|
+
},
|
|
1014
|
+
"Support.RECOVERY_REQUEST_REJECT_FAILED": {
|
|
1015
|
+
"de": "Die Anfrage konnte nicht abgelehnt werden.",
|
|
1016
|
+
"fr": "La demande n’a pas pu être refusée.",
|
|
1017
|
+
"en": "Failed to reject the recovery request."
|
|
1018
|
+
},
|
|
1019
|
+
"Support.RECOVERY_REQUEST_APPROVE_FAILED": {
|
|
1020
|
+
"de": "Die Anfrage konnte nicht akzeptiert werden.",
|
|
1021
|
+
"fr": "La demande n’a pas pu être acceptée.",
|
|
1022
|
+
"en": "Failed to approve the recovery request."
|
|
1023
|
+
},
|
|
1024
|
+
"Auth.MFA_HELP_DIALOG_TITLE": {
|
|
1025
|
+
"de": "Hilfe bei der Anmeldung",
|
|
1026
|
+
"fr": "Aide pour la connexion",
|
|
1027
|
+
"en": "Help with sign-in"
|
|
1028
|
+
},
|
|
1029
|
+
"Auth.MFA_HELP_DIALOG_DESCRIPTION": {
|
|
1030
|
+
"de": "Beschreiben Sie kurz, warum Sie die verfügbaren Methoden nicht nutzen können. Eine Support-Person wird Ihre Anfrage prüfen.",
|
|
1031
|
+
"fr": "Décrivez brièvement pourquoi vous ne pouvez pas utiliser les méthodes disponibles. Une personne du support examinera votre demande.",
|
|
1032
|
+
"en": "Briefly describe why you cannot use the available methods. A support person will review your request."
|
|
1033
|
+
},
|
|
1034
|
+
"Auth.MFA_HELP_MESSAGE_LABEL": {
|
|
1035
|
+
"de": "Ihre Nachricht an den Support",
|
|
1036
|
+
"fr": "Votre message au support",
|
|
1037
|
+
"en": "Your message to support"
|
|
1038
|
+
},
|
|
1039
|
+
"Auth.MFA_HELP_MESSAGE_HELP": {
|
|
1040
|
+
"de": "Optional, hilft dem Support aber bei der Überprüfung Ihrer Identität.",
|
|
1041
|
+
"fr": "Optionnel, mais aide le support à vérifier votre identité.",
|
|
1042
|
+
"en": "Optional, but helps support verify your identity."
|
|
1043
|
+
},
|
|
1044
|
+
"Auth.MFA_HELP_SUBMIT": {
|
|
1045
|
+
"de": "Anfrage senden",
|
|
1046
|
+
"fr": "Envoyer la demande",
|
|
1047
|
+
"en": "Send request"
|
|
1048
|
+
},
|
|
1049
|
+
"Auth.MFA_IDENTIFIER_REQUIRED": {
|
|
1050
|
+
"de": "Bitte geben Sie eine E-Mail-Adresse an.",
|
|
1051
|
+
"fr": "Veuillez indiquer une adresse e-mail.",
|
|
1052
|
+
"en": "Please provide an email address."
|
|
1053
|
+
},
|
|
1054
|
+
"Auth.MFA_HELP_REQUESTED": {
|
|
1055
|
+
"de": "Falls ein Konto mit dieser E-Mail existiert, wurde Ihre Anfrage an den Support weitergeleitet.",
|
|
1056
|
+
"fr": "Si un compte existe avec cette adresse e-mail, votre demande a été transmise au support.",
|
|
1057
|
+
"en": "If an account with this email exists, your request has been forwarded to support."
|
|
1058
|
+
},
|
|
1059
|
+
"Auth.MFA_HELP_REQUEST_FAILED": {
|
|
1060
|
+
"de": "Die Anfrage an den Support konnte nicht gesendet werden.",
|
|
1061
|
+
"fr": "Impossible d’envoyer la demande au support.",
|
|
1062
|
+
"en": "Failed to send the request to support."
|
|
858
1063
|
}
|
|
859
1064
|
// ...
|
|
860
1065
|
};
|
package/package.json
CHANGED
package/src/auth/authApi.jsx
CHANGED
|
@@ -800,19 +800,19 @@ export async function fetchRecoveryRequests(status = 'pending') {
|
|
|
800
800
|
return res.data;
|
|
801
801
|
}
|
|
802
802
|
|
|
803
|
-
export async function approveRecoveryRequest(id,
|
|
803
|
+
export async function approveRecoveryRequest(id, supportNote) {
|
|
804
804
|
const res = await axios.post(
|
|
805
805
|
`/api/support/recovery-requests/${id}/approve/`,
|
|
806
|
-
{
|
|
806
|
+
{ support_note: supportNote || '' },
|
|
807
807
|
{ withCredentials: true },
|
|
808
808
|
);
|
|
809
809
|
return res.data;
|
|
810
810
|
}
|
|
811
811
|
|
|
812
|
-
export async function rejectRecoveryRequest(id) {
|
|
812
|
+
export async function rejectRecoveryRequest(id, supportNote) {
|
|
813
813
|
const res = await axios.post(
|
|
814
814
|
`/api/support/recovery-requests/${id}/reject/`,
|
|
815
|
-
{},
|
|
815
|
+
{ support_note: supportNote || '' },
|
|
816
816
|
{ withCredentials: true },
|
|
817
817
|
);
|
|
818
818
|
return res.data;
|
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
Stack,
|
|
9
9
|
Alert,
|
|
10
10
|
Divider,
|
|
11
|
+
Dialog,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
DialogContent,
|
|
14
|
+
DialogActions,
|
|
11
15
|
} from '@mui/material';
|
|
12
16
|
import { useTranslation } from 'react-i18next';
|
|
13
17
|
import { authApi } from '../auth/authApi';
|
|
@@ -20,6 +24,9 @@ const MfaLoginComponent = ({ availableTypes, identifier, onSuccess, onCancel })
|
|
|
20
24
|
const [infoKey, setInfoKey] = useState(null);
|
|
21
25
|
const [helpRequested, setHelpRequested] = useState(false);
|
|
22
26
|
|
|
27
|
+
const [helpDialogOpen, setHelpDialogOpen] = useState(false);
|
|
28
|
+
const [helpMessage, setHelpMessage] = useState('');
|
|
29
|
+
|
|
23
30
|
const types = Array.isArray(availableTypes) ? availableTypes : [];
|
|
24
31
|
const supportsTotpOrRecovery =
|
|
25
32
|
types.includes('totp') || types.includes('recovery_codes');
|
|
@@ -63,14 +70,22 @@ const MfaLoginComponent = ({ availableTypes, identifier, onSuccess, onCancel })
|
|
|
63
70
|
}
|
|
64
71
|
};
|
|
65
72
|
|
|
73
|
+
const openHelpDialog = () => {
|
|
74
|
+
setErrorKey(null);
|
|
75
|
+
setInfoKey(null);
|
|
76
|
+
setHelpDialogOpen(true);
|
|
77
|
+
};
|
|
78
|
+
|
|
66
79
|
const handleNeedHelp = async () => {
|
|
67
80
|
setErrorKey(null);
|
|
68
81
|
setInfoKey(null);
|
|
69
82
|
setSubmitting(true);
|
|
70
83
|
try {
|
|
71
|
-
await authApi.requestMfaSupportHelp(identifier || '');
|
|
84
|
+
await authApi.requestMfaSupportHelp(identifier || '', helpMessage || '');
|
|
72
85
|
setHelpRequested(true);
|
|
73
86
|
setInfoKey('Auth.MFA_HELP_REQUESTED');
|
|
87
|
+
setHelpDialogOpen(false);
|
|
88
|
+
setHelpMessage('');
|
|
74
89
|
} catch (err) {
|
|
75
90
|
setErrorKey(err.code || 'Auth.MFA_HELP_REQUEST_FAILED');
|
|
76
91
|
} finally {
|
|
@@ -162,7 +177,7 @@ const MfaLoginComponent = ({ availableTypes, identifier, onSuccess, onCancel })
|
|
|
162
177
|
size="small"
|
|
163
178
|
variant="outlined"
|
|
164
179
|
color="secondary"
|
|
165
|
-
onClick={
|
|
180
|
+
onClick={openHelpDialog}
|
|
166
181
|
disabled={submitting || helpRequested}
|
|
167
182
|
>
|
|
168
183
|
{t(
|
|
@@ -172,8 +187,57 @@ const MfaLoginComponent = ({ availableTypes, identifier, onSuccess, onCancel })
|
|
|
172
187
|
</Button>
|
|
173
188
|
</Stack>
|
|
174
189
|
</Stack>
|
|
190
|
+
<Dialog
|
|
191
|
+
open={helpDialogOpen}
|
|
192
|
+
onClose={() => setHelpDialogOpen(false)}
|
|
193
|
+
fullWidth
|
|
194
|
+
maxWidth="sm"
|
|
195
|
+
>
|
|
196
|
+
<DialogTitle>
|
|
197
|
+
{t('Auth.MFA_HELP_DIALOG_TITLE', 'Need help with sign-in')}
|
|
198
|
+
</DialogTitle>
|
|
199
|
+
<DialogContent dividers>
|
|
200
|
+
<Typography variant="body2" sx={{ mb: 2 }}>
|
|
201
|
+
{t(
|
|
202
|
+
'Auth.MFA_HELP_DIALOG_DESCRIPTION',
|
|
203
|
+
'Describe briefly why you cannot use the available methods. A support person will review your request.',
|
|
204
|
+
)}
|
|
205
|
+
</Typography>
|
|
206
|
+
<TextField
|
|
207
|
+
label={t('Auth.MFA_HELP_MESSAGE_LABEL', 'Your message to support')}
|
|
208
|
+
helperText={t(
|
|
209
|
+
'Auth.MFA_HELP_MESSAGE_HELP',
|
|
210
|
+
'Optional, but helps support verify your identity.',
|
|
211
|
+
)}
|
|
212
|
+
multiline
|
|
213
|
+
minRows={3}
|
|
214
|
+
fullWidth
|
|
215
|
+
value={helpMessage}
|
|
216
|
+
onChange={(e) => setHelpMessage(e.target.value)}
|
|
217
|
+
disabled={submitting}
|
|
218
|
+
/>
|
|
219
|
+
</DialogContent>
|
|
220
|
+
<DialogActions>
|
|
221
|
+
<Button
|
|
222
|
+
onClick={() => setHelpDialogOpen(false)}
|
|
223
|
+
disabled={submitting}
|
|
224
|
+
>
|
|
225
|
+
{t('Common.CANCEL', 'Cancel')}
|
|
226
|
+
</Button>
|
|
227
|
+
<Button
|
|
228
|
+
onClick={handleNeedHelp}
|
|
229
|
+
disabled={submitting}
|
|
230
|
+
variant="contained"
|
|
231
|
+
>
|
|
232
|
+
{t('Auth.MFA_HELP_SUBMIT', 'Send request')}
|
|
233
|
+
</Button>
|
|
234
|
+
</DialogActions>
|
|
235
|
+
</Dialog>
|
|
175
236
|
</Box>
|
|
237
|
+
|
|
176
238
|
);
|
|
239
|
+
|
|
177
240
|
};
|
|
178
241
|
|
|
242
|
+
|
|
179
243
|
export default MfaLoginComponent;
|
|
@@ -29,11 +29,11 @@ const SupportRecoveryRequestsTab = () => {
|
|
|
29
29
|
const [requests, setRequests] = useState([]);
|
|
30
30
|
const [loading, setLoading] = useState(true);
|
|
31
31
|
const [errorKey, setErrorKey] = useState(null);
|
|
32
|
-
const [statusFilter, setStatusFilter] = useState('pending');
|
|
32
|
+
const [statusFilter, setStatusFilter] = useState('pending');
|
|
33
33
|
|
|
34
|
-
const [
|
|
35
|
-
const [
|
|
36
|
-
const [
|
|
34
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
35
|
+
const [dialogNote, setDialogNote] = useState('');
|
|
36
|
+
const [selectedRequest, setSelectedRequest] = useState(null);
|
|
37
37
|
|
|
38
38
|
const loadRequests = async () => {
|
|
39
39
|
setLoading(true);
|
|
@@ -53,35 +53,37 @@ const SupportRecoveryRequestsTab = () => {
|
|
|
53
53
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
54
54
|
}, [statusFilter]);
|
|
55
55
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
const openDialog = (req) => {
|
|
57
|
+
setSelectedRequest(req);
|
|
58
|
+
setDialogNote('');
|
|
59
|
+
setDialogOpen(true);
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
const closeDialog = () => {
|
|
63
|
+
setDialogOpen(false);
|
|
64
|
+
setSelectedRequest(null);
|
|
65
|
+
setDialogNote('');
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
-
const
|
|
69
|
-
if (!
|
|
68
|
+
const handleApprove = async () => {
|
|
69
|
+
if (!selectedRequest) return;
|
|
70
70
|
setErrorKey(null);
|
|
71
71
|
try {
|
|
72
|
-
await authApi.approveRecoveryRequest(
|
|
72
|
+
await authApi.approveRecoveryRequest(selectedRequest.id, dialogNote);
|
|
73
73
|
await loadRequests();
|
|
74
|
-
|
|
74
|
+
closeDialog();
|
|
75
75
|
} catch (err) {
|
|
76
76
|
setErrorKey(err.code || 'Support.RECOVERY_REQUEST_APPROVE_FAILED');
|
|
77
77
|
}
|
|
78
78
|
};
|
|
79
79
|
|
|
80
|
-
const handleReject = async (
|
|
80
|
+
const handleReject = async () => {
|
|
81
|
+
if (!selectedRequest) return;
|
|
81
82
|
setErrorKey(null);
|
|
82
83
|
try {
|
|
83
|
-
await authApi.rejectRecoveryRequest(id);
|
|
84
|
+
await authApi.rejectRecoveryRequest(selectedRequest.id, dialogNote);
|
|
84
85
|
await loadRequests();
|
|
86
|
+
closeDialog();
|
|
85
87
|
} catch (err) {
|
|
86
88
|
setErrorKey(err.code || 'Support.RECOVERY_REQUEST_REJECT_FAILED');
|
|
87
89
|
}
|
|
@@ -104,7 +106,7 @@ const SupportRecoveryRequestsTab = () => {
|
|
|
104
106
|
<Typography variant="body2" sx={{ mb: 2 }}>
|
|
105
107
|
{t(
|
|
106
108
|
'Support.RECOVERY_REQUESTS_DESCRIPTION',
|
|
107
|
-
'Users who cannot complete MFA can request support. You can
|
|
109
|
+
'Users who cannot complete MFA can request support. You can review their request and approve or reject it.',
|
|
108
110
|
)}
|
|
109
111
|
</Typography>
|
|
110
112
|
|
|
@@ -140,7 +142,10 @@ const SupportRecoveryRequestsTab = () => {
|
|
|
140
142
|
|
|
141
143
|
{requests.length === 0 ? (
|
|
142
144
|
<Typography variant="body2">
|
|
143
|
-
{t(
|
|
145
|
+
{t(
|
|
146
|
+
'Support.RECOVERY_REQUESTS_EMPTY',
|
|
147
|
+
'No recovery requests for this filter.',
|
|
148
|
+
)}
|
|
144
149
|
</Typography>
|
|
145
150
|
) : (
|
|
146
151
|
<TableContainer component={Paper}>
|
|
@@ -172,28 +177,14 @@ const SupportRecoveryRequestsTab = () => {
|
|
|
172
177
|
<TableCell>{req.user_email || req.user}</TableCell>
|
|
173
178
|
<TableCell>{req.status}</TableCell>
|
|
174
179
|
<TableCell>
|
|
175
|
-
<
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
'Support.RECOVERY_ACTION_SEND_LINK',
|
|
184
|
-
'Send recovery link',
|
|
185
|
-
)}
|
|
186
|
-
</Button>
|
|
187
|
-
<Button
|
|
188
|
-
variant="outlined"
|
|
189
|
-
size="small"
|
|
190
|
-
color="error"
|
|
191
|
-
onClick={() => handleReject(req.id)}
|
|
192
|
-
disabled={req.status !== 'pending'}
|
|
193
|
-
>
|
|
194
|
-
{t('Support.RECOVERY_ACTION_REJECT', 'Reject')}
|
|
195
|
-
</Button>
|
|
196
|
-
</Stack>
|
|
180
|
+
<Button
|
|
181
|
+
variant="contained"
|
|
182
|
+
size="small"
|
|
183
|
+
onClick={() => openDialog(req)}
|
|
184
|
+
disabled={req.status !== 'pending'}
|
|
185
|
+
>
|
|
186
|
+
{t('Support.RECOVERY_ACTION_REVIEW', 'Review')}
|
|
187
|
+
</Button>
|
|
197
188
|
</TableCell>
|
|
198
189
|
</TableRow>
|
|
199
190
|
))}
|
|
@@ -202,55 +193,85 @@ const SupportRecoveryRequestsTab = () => {
|
|
|
202
193
|
</TableContainer>
|
|
203
194
|
)}
|
|
204
195
|
|
|
205
|
-
<Dialog
|
|
206
|
-
open={approveDialogOpen}
|
|
207
|
-
onClose={handleCloseApproveDialog}
|
|
208
|
-
fullWidth
|
|
209
|
-
maxWidth="sm"
|
|
210
|
-
>
|
|
196
|
+
<Dialog open={dialogOpen} onClose={closeDialog} fullWidth maxWidth="sm">
|
|
211
197
|
<DialogTitle>
|
|
212
|
-
{t(
|
|
213
|
-
'Support.RECOVERY_APPROVE_DIALOG_TITLE',
|
|
214
|
-
'Confirm account recovery',
|
|
215
|
-
)}
|
|
198
|
+
{t('Support.RECOVERY_REVIEW_DIALOG_TITLE', 'Review recovery request')}
|
|
216
199
|
</DialogTitle>
|
|
217
200
|
<DialogContent dividers>
|
|
218
|
-
|
|
219
|
-
{
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
201
|
+
{selectedRequest && (
|
|
202
|
+
<Box sx={{ mb: 2 }}>
|
|
203
|
+
<Typography variant="body2" sx={{ mb: 1 }}>
|
|
204
|
+
<strong>
|
|
205
|
+
{t('Support.RECOVERY_REVIEW_USER', 'User')}:
|
|
206
|
+
</strong>{' '}
|
|
207
|
+
{selectedRequest.user_email || selectedRequest.user}
|
|
208
|
+
</Typography>
|
|
209
|
+
<Typography variant="body2" sx={{ mb: 1 }}>
|
|
210
|
+
<strong>
|
|
211
|
+
{t('Support.RECOVERY_REVIEW_CREATED', 'Requested at')}:
|
|
212
|
+
</strong>{' '}
|
|
213
|
+
{selectedRequest.created_at
|
|
214
|
+
? new Date(selectedRequest.created_at).toLocaleString()
|
|
215
|
+
: '-'}
|
|
216
|
+
</Typography>
|
|
217
|
+
<Typography variant="body2" sx={{ mb: 1 }}>
|
|
218
|
+
<strong>
|
|
219
|
+
{t(
|
|
220
|
+
'Support.RECOVERY_REVIEW_MESSAGE',
|
|
221
|
+
'User’s explanation',
|
|
222
|
+
)}
|
|
223
|
+
:
|
|
224
|
+
</strong>
|
|
225
|
+
</Typography>
|
|
226
|
+
<Box
|
|
227
|
+
sx={{
|
|
228
|
+
border: 1,
|
|
229
|
+
borderColor: 'divider',
|
|
230
|
+
borderRadius: 1,
|
|
231
|
+
p: 1,
|
|
232
|
+
mb: 2,
|
|
233
|
+
whiteSpace: 'pre-wrap',
|
|
234
|
+
fontSize: '0.875rem',
|
|
235
|
+
}}
|
|
236
|
+
>
|
|
237
|
+
{selectedRequest.message || t('Support.RECOVERY_NO_MESSAGE', 'No message provided.')}
|
|
238
|
+
</Box>
|
|
239
|
+
</Box>
|
|
240
|
+
)}
|
|
224
241
|
|
|
225
242
|
<TextField
|
|
226
243
|
label={t(
|
|
227
|
-
'Support.
|
|
228
|
-
'Reason for
|
|
244
|
+
'Support.RECOVERY_NOTE_LABEL',
|
|
245
|
+
'Reason for your decision',
|
|
229
246
|
)}
|
|
230
247
|
helperText={t(
|
|
231
|
-
'Support.
|
|
232
|
-
'
|
|
248
|
+
'Support.RECOVERY_NOTE_HELP',
|
|
249
|
+
'Explain briefly why you approve or reject this request.',
|
|
233
250
|
)}
|
|
234
251
|
multiline
|
|
235
252
|
minRows={3}
|
|
236
253
|
fullWidth
|
|
237
|
-
value={
|
|
238
|
-
onChange={(e) =>
|
|
254
|
+
value={dialogNote}
|
|
255
|
+
onChange={(e) => setDialogNote(e.target.value)}
|
|
239
256
|
/>
|
|
240
257
|
</DialogContent>
|
|
241
258
|
<DialogActions>
|
|
242
|
-
<Button onClick={
|
|
259
|
+
<Button onClick={closeDialog}>
|
|
243
260
|
{t('Common.CANCEL', 'Cancel')}
|
|
244
261
|
</Button>
|
|
245
262
|
<Button
|
|
246
|
-
onClick={
|
|
263
|
+
onClick={handleReject}
|
|
264
|
+
color="error"
|
|
265
|
+
disabled={!dialogNote.trim()}
|
|
266
|
+
>
|
|
267
|
+
{t('Support.RECOVERY_REJECT_SUBMIT', 'Reject request')}
|
|
268
|
+
</Button>
|
|
269
|
+
<Button
|
|
270
|
+
onClick={handleApprove}
|
|
247
271
|
variant="contained"
|
|
248
|
-
disabled={!
|
|
272
|
+
disabled={!dialogNote.trim()}
|
|
249
273
|
>
|
|
250
|
-
{t(
|
|
251
|
-
'Support.RECOVERY_APPROVE_SUBMIT',
|
|
252
|
-
'Send link',
|
|
253
|
-
)}
|
|
274
|
+
{t('Support.RECOVERY_APPROVE_SUBMIT', 'Approve and send link')}
|
|
254
275
|
</Button>
|
|
255
276
|
</DialogActions>
|
|
256
277
|
</Dialog>
|
|
@@ -258,4 +279,4 @@ const SupportRecoveryRequestsTab = () => {
|
|
|
258
279
|
);
|
|
259
280
|
};
|
|
260
281
|
|
|
261
|
-
export default
|
|
282
|
+
export default SupportRecove
|
|
@@ -902,7 +902,218 @@ export const authTranslations = {
|
|
|
902
902
|
"de": "Link senden",
|
|
903
903
|
"fr": "Envoyer le lien",
|
|
904
904
|
"en": "Send link"
|
|
905
|
-
}
|
|
905
|
+
},
|
|
906
|
+
"Auth.MFA_HELP_DIALOG_TITLE": {
|
|
907
|
+
"de": "Hilfe bei der Anmeldung",
|
|
908
|
+
"fr": "Aide pour la connexion",
|
|
909
|
+
"en": "Need help with sign-in"
|
|
910
|
+
},
|
|
911
|
+
"Auth.MFA_HELP_DIALOG_DESCRIPTION": {
|
|
912
|
+
"de": "Beschreiben Sie kurz, warum Sie keine der verfügbaren Methoden verwenden können. Eine Supportperson wird Ihre Anfrage prüfen.",
|
|
913
|
+
"fr": "Décrivez brièvement pourquoi vous ne pouvez utiliser aucune des méthodes disponibles. Un membre du support examinera votre demande.",
|
|
914
|
+
"en": "Briefly describe why you cannot use any of the available methods. A support person will review your request."
|
|
915
|
+
},
|
|
916
|
+
"Auth.MFA_HELP_MESSAGE_LABEL": {
|
|
917
|
+
"de": "Ihre Nachricht an den Support",
|
|
918
|
+
"fr": "Votre message au support",
|
|
919
|
+
"en": "Your message to support"
|
|
920
|
+
},
|
|
921
|
+
"Auth.MFA_HELP_MESSAGE_HELP": {
|
|
922
|
+
"de": "Optional, hilft dem Support bei der Überprüfung Ihrer Identität.",
|
|
923
|
+
"fr": "Facultatif, mais aide le support à vérifier votre identité.",
|
|
924
|
+
"en": "Optional, but helps support verify your identity."
|
|
925
|
+
},
|
|
926
|
+
"Auth.MFA_HELP_SUBMIT": {
|
|
927
|
+
"de": "Anfrage senden",
|
|
928
|
+
"fr": "Envoyer la demande",
|
|
929
|
+
"en": "Send request"
|
|
930
|
+
},
|
|
931
|
+
|
|
932
|
+
"Support.RECOVERY_NOTE_LABEL": {
|
|
933
|
+
"de": "Begründung für diese Entscheidung",
|
|
934
|
+
"fr": "Raison de cette décision",
|
|
935
|
+
"en": "Reason for this decision"
|
|
936
|
+
},
|
|
937
|
+
"Support.RECOVERY_NOTE_HELP": {
|
|
938
|
+
"de": "Beschreiben Sie kurz, warum Sie diese Anfrage genehmigen oder ablehnen.",
|
|
939
|
+
"fr": "Décrivez brièvement pourquoi vous approuvez ou rejetez cette demande.",
|
|
940
|
+
"en": "Briefly describe why you are approving or rejecting this request."
|
|
941
|
+
},
|
|
942
|
+
"Support.RECOVERY_REJECT_DIALOG_TITLE": {
|
|
943
|
+
"de": "Konto-Wiederherstellung ablehnen",
|
|
944
|
+
"fr": "Refuser la récupération de compte",
|
|
945
|
+
"en": "Reject account recovery"
|
|
946
|
+
},
|
|
947
|
+
"Support.RECOVERY_REJECT_CONFIRM_QUESTION": {
|
|
948
|
+
"de": "Sind Sie sicher, dass Sie diese Wiederherstellungsanfrage ablehnen möchten?",
|
|
949
|
+
"fr": "Êtes-vous sûr de vouloir refuser cette demande de récupération ?",
|
|
950
|
+
"en": "Are you sure you want to reject this recovery request?"
|
|
951
|
+
},
|
|
952
|
+
"Support.RECOVERY_REJECT_SUBMIT": {
|
|
953
|
+
"de": "Anfrage ablehnen",
|
|
954
|
+
"fr": "Refuser la demande",
|
|
955
|
+
"en": "Reject request"
|
|
956
|
+
},
|
|
957
|
+
"Support.RECOVERY_REQUESTS_TITLE": {
|
|
958
|
+
"de": "Anfragen zur Kontowiederherstellung",
|
|
959
|
+
"fr": "Demandes de récupération de compte",
|
|
960
|
+
"en": "Account recovery requests"
|
|
961
|
+
},
|
|
962
|
+
"Support.RECOVERY_REQUESTS_DESCRIPTION": {
|
|
963
|
+
"de": "Benutzer, die die MFA nicht abschliessen können, können hier eine Anfrage stellen. Sie können die Anfrage prüfen und anschliessend akzeptieren oder ablehnen.",
|
|
964
|
+
"fr": "Les utilisateurs qui ne peuvent pas terminer la MFA peuvent envoyer ici une demande. Vous pouvez examiner la demande puis l’accepter ou la refuser.",
|
|
965
|
+
"en": "Users who cannot complete MFA can submit a request here. You can review the request and then approve or reject it."
|
|
966
|
+
},
|
|
967
|
+
"Support.RECOVERY_REQUESTS_LOAD_FAILED": {
|
|
968
|
+
"de": "Die Anfragen zur Kontowiederherstellung konnten nicht geladen werden.",
|
|
969
|
+
"fr": "Impossible de charger les demandes de récupération de compte.",
|
|
970
|
+
"en": "Failed to load account recovery requests."
|
|
971
|
+
},
|
|
972
|
+
"Support.RECOVERY_FILTER_PENDING": {
|
|
973
|
+
"de": "Offen",
|
|
974
|
+
"fr": "Ouvertes",
|
|
975
|
+
"en": "Open"
|
|
976
|
+
},
|
|
977
|
+
"Support.RECOVERY_FILTER_APPROVED": {
|
|
978
|
+
"de": "Akzeptiert",
|
|
979
|
+
"fr": "Acceptées",
|
|
980
|
+
"en": "Approved"
|
|
981
|
+
},
|
|
982
|
+
"Support.RECOVERY_FILTER_REJECTED": {
|
|
983
|
+
"de": "Abgelehnt",
|
|
984
|
+
"fr": "Refusées",
|
|
985
|
+
"en": "Rejected"
|
|
986
|
+
},
|
|
987
|
+
"Support.RECOVERY_REQUESTS_EMPTY": {
|
|
988
|
+
"de": "Für diesen Filter liegen keine Anfragen vor.",
|
|
989
|
+
"fr": "Aucune demande pour ce filtre.",
|
|
990
|
+
"en": "No recovery requests for this filter."
|
|
991
|
+
},
|
|
992
|
+
"Support.RECOVERY_COL_CREATED": {
|
|
993
|
+
"de": "Erstellt",
|
|
994
|
+
"fr": "Créée le",
|
|
995
|
+
"en": "Created"
|
|
996
|
+
},
|
|
997
|
+
"Support.RECOVERY_COL_USER": {
|
|
998
|
+
"de": "Benutzer",
|
|
999
|
+
"fr": "Utilisateur",
|
|
1000
|
+
"en": "User"
|
|
1001
|
+
},
|
|
1002
|
+
"Support.RECOVERY_COL_STATUS": {
|
|
1003
|
+
"de": "Status",
|
|
1004
|
+
"fr": "Statut",
|
|
1005
|
+
"en": "Status"
|
|
1006
|
+
},
|
|
1007
|
+
"Support.RECOVERY_COL_ACTIONS": {
|
|
1008
|
+
"de": "Aktionen",
|
|
1009
|
+
"fr": "Actions",
|
|
1010
|
+
"en": "Actions"
|
|
1011
|
+
},
|
|
1012
|
+
"Support.RECOVERY_ACTION_REVIEW": {
|
|
1013
|
+
"de": "Prüfen",
|
|
1014
|
+
"fr": "Examiner",
|
|
1015
|
+
"en": "Review"
|
|
1016
|
+
},
|
|
1017
|
+
|
|
1018
|
+
"Support.RECOVERY_REVIEW_DIALOG_TITLE": {
|
|
1019
|
+
"de": "Anfrage zur Kontowiederherstellung prüfen",
|
|
1020
|
+
"fr": "Examiner la demande de récupération de compte",
|
|
1021
|
+
"en": "Review recovery request"
|
|
1022
|
+
},
|
|
1023
|
+
"Support.RECOVERY_REVIEW_USER": {
|
|
1024
|
+
"de": "Benutzer",
|
|
1025
|
+
"fr": "Utilisateur",
|
|
1026
|
+
"en": "User"
|
|
1027
|
+
},
|
|
1028
|
+
"Support.RECOVERY_REVIEW_CREATED": {
|
|
1029
|
+
"de": "Angefragt am",
|
|
1030
|
+
"fr": "Demandée le",
|
|
1031
|
+
"en": "Requested at"
|
|
1032
|
+
},
|
|
1033
|
+
"Support.RECOVERY_REVIEW_MESSAGE": {
|
|
1034
|
+
"de": "Begründung des Benutzers",
|
|
1035
|
+
"fr": "Explication de l’utilisateur",
|
|
1036
|
+
"en": "User’s explanation"
|
|
1037
|
+
},
|
|
1038
|
+
"Support.RECOVERY_NO_MESSAGE": {
|
|
1039
|
+
"de": "Keine Begründung angegeben.",
|
|
1040
|
+
"fr": "Aucune explication fournie.",
|
|
1041
|
+
"en": "No message provided."
|
|
1042
|
+
},
|
|
1043
|
+
|
|
1044
|
+
"Support.RECOVERY_NOTE_LABEL": {
|
|
1045
|
+
"de": "Begründung für Ihre Entscheidung",
|
|
1046
|
+
"fr": "Raison de votre décision",
|
|
1047
|
+
"en": "Reason for your decision"
|
|
1048
|
+
},
|
|
1049
|
+
"Support.RECOVERY_NOTE_HELP": {
|
|
1050
|
+
"de": "Beschreiben Sie kurz, warum Sie diese Anfrage akzeptieren oder ablehnen.",
|
|
1051
|
+
"fr": "Décrivez brièvement pourquoi vous acceptez ou refusez cette demande.",
|
|
1052
|
+
"en": "Briefly explain why you approve or reject this request."
|
|
1053
|
+
},
|
|
1054
|
+
|
|
1055
|
+
"Support.RECOVERY_REJECT_SUBMIT": {
|
|
1056
|
+
"de": "Anfrage ablehnen",
|
|
1057
|
+
"fr": "Refuser la demande",
|
|
1058
|
+
"en": "Reject request"
|
|
1059
|
+
},
|
|
1060
|
+
"Support.RECOVERY_APPROVE_SUBMIT": {
|
|
1061
|
+
"de": "Akzeptieren und Link senden",
|
|
1062
|
+
"fr": "Accepter et envoyer le lien",
|
|
1063
|
+
"en": "Approve and send link"
|
|
1064
|
+
},
|
|
1065
|
+
"Support.RECOVERY_REQUEST_REJECT_FAILED": {
|
|
1066
|
+
"de": "Die Anfrage konnte nicht abgelehnt werden.",
|
|
1067
|
+
"fr": "La demande n’a pas pu être refusée.",
|
|
1068
|
+
"en": "Failed to reject the recovery request."
|
|
1069
|
+
},
|
|
1070
|
+
"Support.RECOVERY_REQUEST_APPROVE_FAILED": {
|
|
1071
|
+
"de": "Die Anfrage konnte nicht akzeptiert werden.",
|
|
1072
|
+
"fr": "La demande n’a pas pu être acceptée.",
|
|
1073
|
+
"en": "Failed to approve the recovery request."
|
|
1074
|
+
},
|
|
1075
|
+
|
|
1076
|
+
"Auth.MFA_HELP_DIALOG_TITLE": {
|
|
1077
|
+
"de": "Hilfe bei der Anmeldung",
|
|
1078
|
+
"fr": "Aide pour la connexion",
|
|
1079
|
+
"en": "Help with sign-in"
|
|
1080
|
+
},
|
|
1081
|
+
"Auth.MFA_HELP_DIALOG_DESCRIPTION": {
|
|
1082
|
+
"de": "Beschreiben Sie kurz, warum Sie die verfügbaren Methoden nicht nutzen können. Eine Support-Person wird Ihre Anfrage prüfen.",
|
|
1083
|
+
"fr": "Décrivez brièvement pourquoi vous ne pouvez pas utiliser les méthodes disponibles. Une personne du support examinera votre demande.",
|
|
1084
|
+
"en": "Briefly describe why you cannot use the available methods. A support person will review your request."
|
|
1085
|
+
},
|
|
1086
|
+
"Auth.MFA_HELP_MESSAGE_LABEL": {
|
|
1087
|
+
"de": "Ihre Nachricht an den Support",
|
|
1088
|
+
"fr": "Votre message au support",
|
|
1089
|
+
"en": "Your message to support"
|
|
1090
|
+
},
|
|
1091
|
+
"Auth.MFA_HELP_MESSAGE_HELP": {
|
|
1092
|
+
"de": "Optional, hilft dem Support aber bei der Überprüfung Ihrer Identität.",
|
|
1093
|
+
"fr": "Optionnel, mais aide le support à vérifier votre identité.",
|
|
1094
|
+
"en": "Optional, but helps support verify your identity."
|
|
1095
|
+
},
|
|
1096
|
+
"Auth.MFA_HELP_SUBMIT": {
|
|
1097
|
+
"de": "Anfrage senden",
|
|
1098
|
+
"fr": "Envoyer la demande",
|
|
1099
|
+
"en": "Send request"
|
|
1100
|
+
},
|
|
1101
|
+
|
|
1102
|
+
"Auth.MFA_IDENTIFIER_REQUIRED": {
|
|
1103
|
+
"de": "Bitte geben Sie eine E-Mail-Adresse an.",
|
|
1104
|
+
"fr": "Veuillez indiquer une adresse e-mail.",
|
|
1105
|
+
"en": "Please provide an email address."
|
|
1106
|
+
},
|
|
1107
|
+
"Auth.MFA_HELP_REQUESTED": {
|
|
1108
|
+
"de": "Falls ein Konto mit dieser E-Mail existiert, wurde Ihre Anfrage an den Support weitergeleitet.",
|
|
1109
|
+
"fr": "Si un compte existe avec cette adresse e-mail, votre demande a été transmise au support.",
|
|
1110
|
+
"en": "If an account with this email exists, your request has been forwarded to support."
|
|
1111
|
+
},
|
|
1112
|
+
"Auth.MFA_HELP_REQUEST_FAILED": {
|
|
1113
|
+
"de": "Die Anfrage an den Support konnte nicht gesendet werden.",
|
|
1114
|
+
"fr": "Impossible d’envoyer la demande au support.",
|
|
1115
|
+
"en": "Failed to send the request to support."
|
|
1116
|
+
}
|
|
906
1117
|
|
|
907
1118
|
|
|
908
1119
|
|