@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.
@@ -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, note = '') {
613
- const res = await axios.post(`/api/support/recovery-requests/${id}/approve/`, { note }, // 👉 Note mitsenden
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: handleNeedHelp, disabled: submitting || helpRequested, children: t('Auth.MFA_NEED_HELP', "I can't use any of these methods") })] })] })] }));
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'); // optional erweiterbar
13
- const [approveDialogOpen, setApproveDialogOpen] = useState(false);
14
- const [approveDialogNote, setApproveDialogNote] = useState('');
15
- const [approveDialogRequestId, setApproveDialogRequestId] = useState(null);
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 handleOpenApproveDialog = (id) => {
35
- setApproveDialogRequestId(id);
36
- setApproveDialogNote('');
37
- setApproveDialogOpen(true);
34
+ const openDialog = (req) => {
35
+ setSelectedRequest(req);
36
+ setDialogNote('');
37
+ setDialogOpen(true);
38
38
  };
39
- const handleCloseApproveDialog = () => {
40
- setApproveDialogOpen(false);
41
- setApproveDialogRequestId(null);
42
- setApproveDialogNote('');
39
+ const closeDialog = () => {
40
+ setDialogOpen(false);
41
+ setSelectedRequest(null);
42
+ setDialogNote('');
43
43
  };
44
- const handleConfirmApprove = async () => {
45
- if (!approveDialogRequestId)
44
+ const handleApprove = async () => {
45
+ if (!selectedRequest)
46
46
  return;
47
47
  setErrorKey(null);
48
48
  try {
49
- await authApi.approveRecoveryRequest(approveDialogRequestId, approveDialogNote);
49
+ await authApi.approveRecoveryRequest(selectedRequest.id, dialogNote);
50
50
  await loadRequests();
51
- handleCloseApproveDialog();
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 (id) => {
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 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
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: _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Button, { variant: "contained", size: "small", onClick: () => handleOpenApproveDialog(req.id), disabled: req.status !== 'pending', children: t('Support.RECOVERY_ACTION_SEND_LINK', 'Send recovery link') }), _jsx(Button, { variant: "outlined", size: "small", color: "error", onClick: () => handleReject(req.id), disabled: req.status !== 'pending', children: t('Support.RECOVERY_ACTION_REJECT', 'Reject') })] }) })] }, req.id))) })] }) })), _jsxs(Dialog, { open: approveDialogOpen, onClose: handleCloseApproveDialog, fullWidth: true, maxWidth: "sm", children: [_jsx(DialogTitle, { children: t('Support.RECOVERY_APPROVE_DIALOG_TITLE', 'Confirm account recovery') }), _jsxs(DialogContent, { dividers: true, children: [_jsx(Typography, { variant: "body2", sx: { mb: 2 }, children: t('Support.RECOVERY_APPROVE_CONFIRM_QUESTION', 'Are you sure you want to approve this recovery request?') }), _jsx(TextField, { label: t('Support.RECOVERY_APPROVE_NOTE_LABEL', 'Reason for this decision'), helperText: t('Support.RECOVERY_APPROVE_NOTE_HELP', 'Describe briefly why you are approving this request.'), multiline: true, minRows: 3, fullWidth: true, value: approveDialogNote, onChange: (e) => setApproveDialogNote(e.target.value) })] }), _jsxs(DialogActions, { children: [_jsx(Button, { onClick: handleCloseApproveDialog, children: t('Common.CANCEL', 'Cancel') }), _jsx(Button, { onClick: handleConfirmApprove, variant: "contained", disabled: !approveDialogNote.trim(), children: t('Support.RECOVERY_APPROVE_SUBMIT', 'Send link') })] })] })] }));
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 SupportRecoveryRequestsTab;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "1.4.11",
3
+ "version": "1.4.13",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "private": false,
@@ -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, note = '') {
803
+ export async function approveRecoveryRequest(id, supportNote) {
804
804
  const res = await axios.post(
805
805
  `/api/support/recovery-requests/${id}/approve/`,
806
- { note }, // 👉 Note mitsenden
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={handleNeedHelp}
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'); // optional erweiterbar
32
+ const [statusFilter, setStatusFilter] = useState('pending');
33
33
 
34
- const [approveDialogOpen, setApproveDialogOpen] = useState(false);
35
- const [approveDialogNote, setApproveDialogNote] = useState('');
36
- const [approveDialogRequestId, setApproveDialogRequestId] = useState(null);
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 handleOpenApproveDialog = (id) => {
57
- setApproveDialogRequestId(id);
58
- setApproveDialogNote('');
59
- setApproveDialogOpen(true);
56
+ const openDialog = (req) => {
57
+ setSelectedRequest(req);
58
+ setDialogNote('');
59
+ setDialogOpen(true);
60
60
  };
61
61
 
62
- const handleCloseApproveDialog = () => {
63
- setApproveDialogOpen(false);
64
- setApproveDialogRequestId(null);
65
- setApproveDialogNote('');
62
+ const closeDialog = () => {
63
+ setDialogOpen(false);
64
+ setSelectedRequest(null);
65
+ setDialogNote('');
66
66
  };
67
67
 
68
- const handleConfirmApprove = async () => {
69
- if (!approveDialogRequestId) return;
68
+ const handleApprove = async () => {
69
+ if (!selectedRequest) return;
70
70
  setErrorKey(null);
71
71
  try {
72
- await authApi.approveRecoveryRequest(approveDialogRequestId, approveDialogNote);
72
+ await authApi.approveRecoveryRequest(selectedRequest.id, dialogNote);
73
73
  await loadRequests();
74
- handleCloseApproveDialog();
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 (id) => {
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 send them a recovery link or reject the request after verification.',
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('Support.RECOVERY_REQUESTS_EMPTY', 'No recovery requests for this filter.')}
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
- <Stack direction="row" spacing={1}>
176
- <Button
177
- variant="contained"
178
- size="small"
179
- onClick={() => handleOpenApproveDialog(req.id)}
180
- disabled={req.status !== 'pending'}
181
- >
182
- {t(
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
- <Typography variant="body2" sx={{ mb: 2 }}>
219
- {t(
220
- 'Support.RECOVERY_APPROVE_CONFIRM_QUESTION',
221
- 'Are you sure you want to approve this recovery request?',
222
- )}
223
- </Typography>
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.RECOVERY_APPROVE_NOTE_LABEL',
228
- 'Reason for this decision',
244
+ 'Support.RECOVERY_NOTE_LABEL',
245
+ 'Reason for your decision',
229
246
  )}
230
247
  helperText={t(
231
- 'Support.RECOVERY_APPROVE_NOTE_HELP',
232
- 'Describe briefly why you are approving this request.',
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={approveDialogNote}
238
- onChange={(e) => setApproveDialogNote(e.target.value)}
254
+ value={dialogNote}
255
+ onChange={(e) => setDialogNote(e.target.value)}
239
256
  />
240
257
  </DialogContent>
241
258
  <DialogActions>
242
- <Button onClick={handleCloseApproveDialog}>
259
+ <Button onClick={closeDialog}>
243
260
  {t('Common.CANCEL', 'Cancel')}
244
261
  </Button>
245
262
  <Button
246
- onClick={handleConfirmApprove}
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={!approveDialogNote.trim()}
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 SupportRecoveryRequestsTab;
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