@micha.bigler/ui-core-micha 1.4.11 → 1.4.12
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 -28
- package/dist/i18n/authTranslations.js +50 -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 +65 -56
- package/src/i18n/authTranslations.js +51 -0
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,11 @@ 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 [dialogRequestId, setDialogRequestId] = useState(null);
|
|
16
|
+
const [dialogMode, setDialogMode] = useState('approve');
|
|
16
17
|
const loadRequests = async () => {
|
|
17
18
|
setLoading(true);
|
|
18
19
|
setErrorKey(null);
|
|
@@ -31,37 +32,36 @@ const SupportRecoveryRequestsTab = () => {
|
|
|
31
32
|
loadRequests();
|
|
32
33
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
33
34
|
}, [statusFilter]);
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
const openDialog = (id, mode) => {
|
|
36
|
+
setDialogRequestId(id);
|
|
37
|
+
setDialogMode(mode);
|
|
38
|
+
setDialogNote('');
|
|
39
|
+
setDialogOpen(true);
|
|
38
40
|
};
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const closeDialog = () => {
|
|
42
|
+
setDialogOpen(false);
|
|
43
|
+
setDialogRequestId(null);
|
|
44
|
+
setDialogNote('');
|
|
43
45
|
};
|
|
44
|
-
const
|
|
45
|
-
if (!
|
|
46
|
+
const handleConfirm = async () => {
|
|
47
|
+
if (!dialogRequestId)
|
|
46
48
|
return;
|
|
47
49
|
setErrorKey(null);
|
|
48
50
|
try {
|
|
49
|
-
|
|
51
|
+
if (dialogMode === 'approve') {
|
|
52
|
+
await authApi.approveRecoveryRequest(dialogRequestId, dialogNote);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
await authApi.rejectRecoveryRequest(dialogRequestId, dialogNote);
|
|
56
|
+
}
|
|
50
57
|
await loadRequests();
|
|
51
|
-
|
|
58
|
+
closeDialog();
|
|
52
59
|
}
|
|
53
60
|
catch (err) {
|
|
54
|
-
setErrorKey(err.code ||
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
setErrorKey(null);
|
|
59
|
-
try {
|
|
60
|
-
await authApi.rejectRecoveryRequest(id);
|
|
61
|
-
await loadRequests();
|
|
62
|
-
}
|
|
63
|
-
catch (err) {
|
|
64
|
-
setErrorKey(err.code || 'Support.RECOVERY_REQUEST_REJECT_FAILED');
|
|
61
|
+
setErrorKey(err.code ||
|
|
62
|
+
(dialogMode === 'approve'
|
|
63
|
+
? 'Support.RECOVERY_REQUEST_APPROVE_FAILED'
|
|
64
|
+
: 'Support.RECOVERY_REQUEST_REJECT_FAILED'));
|
|
65
65
|
}
|
|
66
66
|
};
|
|
67
67
|
if (loading) {
|
|
@@ -69,6 +69,12 @@ const SupportRecoveryRequestsTab = () => {
|
|
|
69
69
|
}
|
|
70
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 send them a recovery link or reject the request after verification.') }), 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
71
|
? new Date(req.created_at).toLocaleString()
|
|
72
|
-
: '-' }), _jsx(TableCell, { children: req.user_email || req.user }), _jsx(TableCell, { children: req.status }), _jsx(TableCell, { children: _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Button, { variant: "contained", size: "small", onClick: () =>
|
|
72
|
+
: '-' }), _jsx(TableCell, { children: req.user_email || req.user }), _jsx(TableCell, { children: req.status }), _jsx(TableCell, { children: _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Button, { variant: "contained", size: "small", onClick: () => openDialog(req.id, 'approve'), disabled: req.status !== 'pending', children: t('Support.RECOVERY_ACTION_SEND_LINK', 'Send recovery link') }), _jsx(Button, { variant: "outlined", size: "small", color: "error", onClick: () => openDialog(req.id, 'reject'), disabled: req.status !== 'pending', children: t('Support.RECOVERY_ACTION_REJECT', 'Reject') })] }) })] }, req.id))) })] }) })), _jsxs(Dialog, { open: dialogOpen, onClose: closeDialog, fullWidth: true, maxWidth: "sm", children: [_jsx(DialogTitle, { children: dialogMode === 'approve'
|
|
73
|
+
? t('Support.RECOVERY_APPROVE_DIALOG_TITLE', 'Confirm account recovery')
|
|
74
|
+
: t('Support.RECOVERY_REJECT_DIALOG_TITLE', 'Reject account recovery') }), _jsxs(DialogContent, { dividers: true, children: [_jsx(Typography, { variant: "body2", sx: { mb: 2 }, children: dialogMode === 'approve'
|
|
75
|
+
? t('Support.RECOVERY_APPROVE_CONFIRM_QUESTION', 'Are you sure you want to approve this recovery request?')
|
|
76
|
+
: t('Support.RECOVERY_REJECT_CONFIRM_QUESTION', 'Are you sure you want to reject this recovery request?') }), _jsx(TextField, { label: t('Support.RECOVERY_NOTE_LABEL', 'Reason for this decision'), helperText: t('Support.RECOVERY_NOTE_HELP', 'Describe briefly why you are approving or rejecting 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: handleConfirm, variant: "contained", disabled: !dialogNote.trim(), color: dialogMode === 'approve' ? 'primary' : 'error', children: dialogMode === 'approve'
|
|
77
|
+
? t('Support.RECOVERY_APPROVE_SUBMIT', 'Send link')
|
|
78
|
+
: t('Support.RECOVERY_REJECT_SUBMIT', 'Reject request') })] })] })] }));
|
|
73
79
|
};
|
|
74
80
|
export default SupportRecoveryRequestsTab;
|
|
@@ -855,6 +855,56 @@ 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"
|
|
858
908
|
}
|
|
859
909
|
// ...
|
|
860
910
|
};
|
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,12 @@ 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 [dialogRequestId, setDialogRequestId] = useState(null);
|
|
37
|
+
const [dialogMode, setDialogMode] = useState('approve');
|
|
37
38
|
|
|
38
39
|
const loadRequests = async () => {
|
|
39
40
|
setLoading(true);
|
|
@@ -53,37 +54,37 @@ const SupportRecoveryRequestsTab = () => {
|
|
|
53
54
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
54
55
|
}, [statusFilter]);
|
|
55
56
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
const openDialog = (id, mode) => {
|
|
58
|
+
setDialogRequestId(id);
|
|
59
|
+
setDialogMode(mode);
|
|
60
|
+
setDialogNote('');
|
|
61
|
+
setDialogOpen(true);
|
|
60
62
|
};
|
|
61
63
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
const closeDialog = () => {
|
|
65
|
+
setDialogOpen(false);
|
|
66
|
+
setDialogRequestId(null);
|
|
67
|
+
setDialogNote('');
|
|
66
68
|
};
|
|
67
69
|
|
|
68
|
-
const
|
|
69
|
-
if (!
|
|
70
|
+
const handleConfirm = async () => {
|
|
71
|
+
if (!dialogRequestId) return;
|
|
70
72
|
setErrorKey(null);
|
|
71
73
|
try {
|
|
72
|
-
|
|
74
|
+
if (dialogMode === 'approve') {
|
|
75
|
+
await authApi.approveRecoveryRequest(dialogRequestId, dialogNote);
|
|
76
|
+
} else {
|
|
77
|
+
await authApi.rejectRecoveryRequest(dialogRequestId, dialogNote);
|
|
78
|
+
}
|
|
73
79
|
await loadRequests();
|
|
74
|
-
|
|
80
|
+
closeDialog();
|
|
75
81
|
} catch (err) {
|
|
76
|
-
setErrorKey(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
await authApi.rejectRecoveryRequest(id);
|
|
84
|
-
await loadRequests();
|
|
85
|
-
} catch (err) {
|
|
86
|
-
setErrorKey(err.code || 'Support.RECOVERY_REQUEST_REJECT_FAILED');
|
|
82
|
+
setErrorKey(
|
|
83
|
+
err.code ||
|
|
84
|
+
(dialogMode === 'approve'
|
|
85
|
+
? 'Support.RECOVERY_REQUEST_APPROVE_FAILED'
|
|
86
|
+
: 'Support.RECOVERY_REQUEST_REJECT_FAILED'),
|
|
87
|
+
);
|
|
87
88
|
}
|
|
88
89
|
};
|
|
89
90
|
|
|
@@ -140,7 +141,10 @@ const SupportRecoveryRequestsTab = () => {
|
|
|
140
141
|
|
|
141
142
|
{requests.length === 0 ? (
|
|
142
143
|
<Typography variant="body2">
|
|
143
|
-
{t(
|
|
144
|
+
{t(
|
|
145
|
+
'Support.RECOVERY_REQUESTS_EMPTY',
|
|
146
|
+
'No recovery requests for this filter.',
|
|
147
|
+
)}
|
|
144
148
|
</Typography>
|
|
145
149
|
) : (
|
|
146
150
|
<TableContainer component={Paper}>
|
|
@@ -176,7 +180,7 @@ const SupportRecoveryRequestsTab = () => {
|
|
|
176
180
|
<Button
|
|
177
181
|
variant="contained"
|
|
178
182
|
size="small"
|
|
179
|
-
onClick={() =>
|
|
183
|
+
onClick={() => openDialog(req.id, 'approve')}
|
|
180
184
|
disabled={req.status !== 'pending'}
|
|
181
185
|
>
|
|
182
186
|
{t(
|
|
@@ -188,7 +192,7 @@ const SupportRecoveryRequestsTab = () => {
|
|
|
188
192
|
variant="outlined"
|
|
189
193
|
size="small"
|
|
190
194
|
color="error"
|
|
191
|
-
onClick={() =>
|
|
195
|
+
onClick={() => openDialog(req.id, 'reject')}
|
|
192
196
|
disabled={req.status !== 'pending'}
|
|
193
197
|
>
|
|
194
198
|
{t('Support.RECOVERY_ACTION_REJECT', 'Reject')}
|
|
@@ -202,55 +206,60 @@ const SupportRecoveryRequestsTab = () => {
|
|
|
202
206
|
</TableContainer>
|
|
203
207
|
)}
|
|
204
208
|
|
|
205
|
-
<Dialog
|
|
206
|
-
open={approveDialogOpen}
|
|
207
|
-
onClose={handleCloseApproveDialog}
|
|
208
|
-
fullWidth
|
|
209
|
-
maxWidth="sm"
|
|
210
|
-
>
|
|
209
|
+
<Dialog open={dialogOpen} onClose={closeDialog} fullWidth maxWidth="sm">
|
|
211
210
|
<DialogTitle>
|
|
212
|
-
{
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
211
|
+
{dialogMode === 'approve'
|
|
212
|
+
? t(
|
|
213
|
+
'Support.RECOVERY_APPROVE_DIALOG_TITLE',
|
|
214
|
+
'Confirm account recovery',
|
|
215
|
+
)
|
|
216
|
+
: t(
|
|
217
|
+
'Support.RECOVERY_REJECT_DIALOG_TITLE',
|
|
218
|
+
'Reject account recovery',
|
|
219
|
+
)}
|
|
216
220
|
</DialogTitle>
|
|
217
221
|
<DialogContent dividers>
|
|
218
222
|
<Typography variant="body2" sx={{ mb: 2 }}>
|
|
219
|
-
{
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
+
{dialogMode === 'approve'
|
|
224
|
+
? t(
|
|
225
|
+
'Support.RECOVERY_APPROVE_CONFIRM_QUESTION',
|
|
226
|
+
'Are you sure you want to approve this recovery request?',
|
|
227
|
+
)
|
|
228
|
+
: t(
|
|
229
|
+
'Support.RECOVERY_REJECT_CONFIRM_QUESTION',
|
|
230
|
+
'Are you sure you want to reject this recovery request?',
|
|
231
|
+
)}
|
|
223
232
|
</Typography>
|
|
224
233
|
|
|
225
234
|
<TextField
|
|
226
235
|
label={t(
|
|
227
|
-
'Support.
|
|
236
|
+
'Support.RECOVERY_NOTE_LABEL',
|
|
228
237
|
'Reason for this decision',
|
|
229
238
|
)}
|
|
230
239
|
helperText={t(
|
|
231
|
-
'Support.
|
|
232
|
-
'Describe briefly why you are approving this request.',
|
|
240
|
+
'Support.RECOVERY_NOTE_HELP',
|
|
241
|
+
'Describe briefly why you are approving or rejecting this request.',
|
|
233
242
|
)}
|
|
234
243
|
multiline
|
|
235
244
|
minRows={3}
|
|
236
245
|
fullWidth
|
|
237
|
-
value={
|
|
238
|
-
onChange={(e) =>
|
|
246
|
+
value={dialogNote}
|
|
247
|
+
onChange={(e) => setDialogNote(e.target.value)}
|
|
239
248
|
/>
|
|
240
249
|
</DialogContent>
|
|
241
250
|
<DialogActions>
|
|
242
|
-
<Button onClick={
|
|
251
|
+
<Button onClick={closeDialog}>
|
|
243
252
|
{t('Common.CANCEL', 'Cancel')}
|
|
244
253
|
</Button>
|
|
245
254
|
<Button
|
|
246
|
-
onClick={
|
|
255
|
+
onClick={handleConfirm}
|
|
247
256
|
variant="contained"
|
|
248
|
-
disabled={!
|
|
257
|
+
disabled={!dialogNote.trim()}
|
|
258
|
+
color={dialogMode === 'approve' ? 'primary' : 'error'}
|
|
249
259
|
>
|
|
250
|
-
{
|
|
251
|
-
'Support.RECOVERY_APPROVE_SUBMIT',
|
|
252
|
-
'
|
|
253
|
-
)}
|
|
260
|
+
{dialogMode === 'approve'
|
|
261
|
+
? t('Support.RECOVERY_APPROVE_SUBMIT', 'Send link')
|
|
262
|
+
: t('Support.RECOVERY_REJECT_SUBMIT', 'Reject request')}
|
|
254
263
|
</Button>
|
|
255
264
|
</DialogActions>
|
|
256
265
|
</Dialog>
|
|
@@ -902,6 +902,57 @@ export const authTranslations = {
|
|
|
902
902
|
"de": "Link senden",
|
|
903
903
|
"fr": "Envoyer le lien",
|
|
904
904
|
"en": "Send link"
|
|
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"
|
|
905
956
|
}
|
|
906
957
|
|
|
907
958
|
|