@micha.bigler/ui-core-micha 2.2.6 → 2.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/AuthContext.js +2 -2
- package/dist/components/AllowedEmailDomainsManager.js +2 -2
- package/dist/components/AuthFactorRequirementCard.js +10 -3
- package/dist/components/QrSignupValidityManager.js +2 -2
- package/dist/components/RegistrationMethodsManager.js +2 -2
- package/dist/components/SupportRecoveryRequestsTab.js +94 -4
- package/dist/components/UserListComponent.js +16 -21
- package/dist/pages/AccountPage.js +13 -6
- package/package.json +1 -1
- package/src/auth/AuthContext.jsx +1 -0
- package/src/components/AllowedEmailDomainsManager.jsx +3 -2
- package/src/components/AuthFactorRequirementCard.jsx +11 -4
- package/src/components/QrSignupValidityManager.jsx +3 -2
- package/src/components/RegistrationMethodsManager.jsx +6 -5
- package/src/components/SupportRecoveryRequestsTab.jsx +208 -14
- package/src/components/UserListComponent.jsx +17 -33
- package/src/pages/AccountPage.jsx +24 -14
package/dist/auth/AuthContext.js
CHANGED
|
@@ -26,9 +26,9 @@ export const AuthProvider = ({ children }) => {
|
|
|
26
26
|
const [authMethods, setAuthMethods] = useState(DEFAULT_AUTH_METHODS);
|
|
27
27
|
const [loading, setLoading] = useState(true);
|
|
28
28
|
const mapUserFromApi = (data) => {
|
|
29
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
29
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
30
30
|
const profile = (data === null || data === void 0 ? void 0 : data.profile) || {};
|
|
31
|
-
return Object.assign(Object.assign({}, data), { id: data === null || data === void 0 ? void 0 : data.id, username: data === null || data === void 0 ? void 0 : data.username, email: data === null || data === void 0 ? void 0 : data.email, first_name: data === null || data === void 0 ? void 0 : data.first_name, last_name: data === null || data === void 0 ? void 0 : data.last_name, role: (_b = (_a = data === null || data === void 0 ? void 0 : data.role) !== null && _a !== void 0 ? _a : profile === null || profile === void 0 ? void 0 : profile.role) !== null && _b !== void 0 ? _b : null, language: (_d = (_c = data === null || data === void 0 ? void 0 : data.language) !== null && _c !== void 0 ? _c : profile === null || profile === void 0 ? void 0 : profile.language) !== null && _d !== void 0 ? _d : 'en', is_superuser: Boolean(data === null || data === void 0 ? void 0 : data.is_superuser), is_new: Boolean((_e = data === null || data === void 0 ? void 0 : data.is_new) !== null && _e !== void 0 ? _e : profile === null || profile === void 0 ? void 0 : profile.is_new), is_invited: Boolean((_f = data === null || data === void 0 ? void 0 : data.is_invited) !== null && _f !== void 0 ? _f : profile === null || profile === void 0 ? void 0 : profile.is_invited), accepted_privacy_statement: Boolean((_g = data === null || data === void 0 ? void 0 : data.accepted_privacy_statement) !== null && _g !== void 0 ? _g : profile === null || profile === void 0 ? void 0 : profile.accepted_privacy_statement), accepted_convenience_cookies: Boolean((_h = data === null || data === void 0 ? void 0 : data.accepted_convenience_cookies) !== null && _h !== void 0 ? _h : profile === null || profile === void 0 ? void 0 : profile.accepted_convenience_cookies), is_support_agent: Boolean((_j = data === null || data === void 0 ? void 0 : data.is_support_agent) !== null && _j !== void 0 ? _j : profile === null || profile === void 0 ? void 0 : profile.is_support_agent), support_contact_id: (_l = (_k = data === null || data === void 0 ? void 0 : data.support_contact_id) !== null && _k !== void 0 ? _k : profile === null || profile === void 0 ? void 0 : profile.support_contact_id) !== null && _l !== void 0 ? _l : null, security_state: data === null || data === void 0 ? void 0 : data.security_state, available_roles: (data === null || data === void 0 ? void 0 : data.available_roles) || [], ui_permissions: (data === null || data === void 0 ? void 0 : data.ui_permissions) || {}, can_manage_support_agents: Boolean(data === null || data === void 0 ? void 0 : data.can_manage_support_agents), can_manage: Boolean(data === null || data === void 0 ? void 0 : data.can_manage), is_active: data === null || data === void 0 ? void 0 : data.is_active, last_login: data === null || data === void 0 ? void 0 : data.last_login, date_joined: data === null || data === void 0 ? void 0 : data.date_joined });
|
|
31
|
+
return Object.assign(Object.assign({}, data), { id: data === null || data === void 0 ? void 0 : data.id, username: data === null || data === void 0 ? void 0 : data.username, email: data === null || data === void 0 ? void 0 : data.email, first_name: data === null || data === void 0 ? void 0 : data.first_name, last_name: data === null || data === void 0 ? void 0 : data.last_name, role: (_b = (_a = data === null || data === void 0 ? void 0 : data.role) !== null && _a !== void 0 ? _a : profile === null || profile === void 0 ? void 0 : profile.role) !== null && _b !== void 0 ? _b : null, language: (_d = (_c = data === null || data === void 0 ? void 0 : data.language) !== null && _c !== void 0 ? _c : profile === null || profile === void 0 ? void 0 : profile.language) !== null && _d !== void 0 ? _d : 'en', is_superuser: Boolean(data === null || data === void 0 ? void 0 : data.is_superuser), is_new: Boolean((_e = data === null || data === void 0 ? void 0 : data.is_new) !== null && _e !== void 0 ? _e : profile === null || profile === void 0 ? void 0 : profile.is_new), is_invited: Boolean((_f = data === null || data === void 0 ? void 0 : data.is_invited) !== null && _f !== void 0 ? _f : profile === null || profile === void 0 ? void 0 : profile.is_invited), accepted_privacy_statement: Boolean((_g = data === null || data === void 0 ? void 0 : data.accepted_privacy_statement) !== null && _g !== void 0 ? _g : profile === null || profile === void 0 ? void 0 : profile.accepted_privacy_statement), accepted_convenience_cookies: Boolean((_h = data === null || data === void 0 ? void 0 : data.accepted_convenience_cookies) !== null && _h !== void 0 ? _h : profile === null || profile === void 0 ? void 0 : profile.accepted_convenience_cookies), is_support_agent: Boolean((_j = data === null || data === void 0 ? void 0 : data.is_support_agent) !== null && _j !== void 0 ? _j : profile === null || profile === void 0 ? void 0 : profile.is_support_agent), support_contact_id: (_l = (_k = data === null || data === void 0 ? void 0 : data.support_contact_id) !== null && _k !== void 0 ? _k : profile === null || profile === void 0 ? void 0 : profile.support_contact_id) !== null && _l !== void 0 ? _l : null, security_state: data === null || data === void 0 ? void 0 : data.security_state, available_roles: (data === null || data === void 0 ? void 0 : data.available_roles) || [], ui_permissions: (data === null || data === void 0 ? void 0 : data.ui_permissions) || {}, can_manage_support_agents: Boolean(data === null || data === void 0 ? void 0 : data.can_manage_support_agents), can_manage: Boolean(data === null || data === void 0 ? void 0 : data.can_manage), is_active: data === null || data === void 0 ? void 0 : data.is_active, successful_login: Boolean((_m = data === null || data === void 0 ? void 0 : data.successful_login) !== null && _m !== void 0 ? _m : data === null || data === void 0 ? void 0 : data.last_login), last_login: data === null || data === void 0 ? void 0 : data.last_login, date_joined: data === null || data === void 0 ? void 0 : data.date_joined });
|
|
32
32
|
};
|
|
33
33
|
useEffect(() => {
|
|
34
34
|
let isMounted = true;
|
|
@@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react';
|
|
|
3
3
|
import { Alert, Box, Button, TextField, Typography, } from '@mui/material';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import { updateAuthPolicy } from '../auth/authApi';
|
|
6
|
-
export function AllowedEmailDomainsManager({ domains = [], enabled = false, onPolicyChange, }) {
|
|
6
|
+
export function AllowedEmailDomainsManager({ domains = [], enabled = false, onPolicyChange, canEdit = true, }) {
|
|
7
7
|
const { t } = useTranslation();
|
|
8
8
|
const [domainsText, setDomainsText] = useState('');
|
|
9
9
|
const [busy, setBusy] = useState(false);
|
|
@@ -39,5 +39,5 @@ export function AllowedEmailDomainsManager({ domains = [], enabled = false, onPo
|
|
|
39
39
|
if (!enabled) {
|
|
40
40
|
return null;
|
|
41
41
|
}
|
|
42
|
-
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.ALLOWED_EMAIL_DOMAINS_TITLE', 'Allowed Email Domains') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Auth.ALLOWED_EMAIL_DOMAINS_CARD_HINT', 'Only addresses from these domains can use email-domain sign-up.') }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error }), success && _jsx(Alert, { severity: "success", sx: { mb: 2 }, children: success }), _jsx(TextField, { label: t('Auth.ALLOWED_EMAIL_DOMAINS_LABEL', 'Allowed email domains'), helperText: t('Auth.ALLOWED_EMAIL_DOMAINS_HINT', 'One domain per line, e.g. example.org. You can leave this empty temporarily.'), multiline: true, minRows: 4, fullWidth: true, value: domainsText, onChange: (event) => setDomainsText(event.target.value), disabled: busy }), _jsx(Button, { variant: "contained", sx: { mt: 2 }, onClick: handleSave, disabled: busy, children: t('Common.SAVE', 'Save') })] }));
|
|
42
|
+
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.ALLOWED_EMAIL_DOMAINS_TITLE', 'Allowed Email Domains') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Auth.ALLOWED_EMAIL_DOMAINS_CARD_HINT', 'Only addresses from these domains can use email-domain sign-up.') }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error }), success && _jsx(Alert, { severity: "success", sx: { mb: 2 }, children: success }), _jsx(TextField, { label: t('Auth.ALLOWED_EMAIL_DOMAINS_LABEL', 'Allowed email domains'), helperText: t('Auth.ALLOWED_EMAIL_DOMAINS_HINT', 'One domain per line, e.g. example.org. You can leave this empty temporarily.'), multiline: true, minRows: 4, fullWidth: true, value: domainsText, onChange: (event) => setDomainsText(event.target.value), disabled: busy || !canEdit }), _jsx(Button, { variant: "contained", sx: { mt: 2 }, onClick: handleSave, disabled: busy || !canEdit, children: t('Common.SAVE', 'Save') })] }));
|
|
43
43
|
}
|
|
@@ -3,13 +3,17 @@ import React, { useEffect, useState } from 'react';
|
|
|
3
3
|
import { Alert, Box, FormControl, FormControlLabel, Radio, RadioGroup, Typography, } from '@mui/material';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import { fetchAuthPolicy, updateAuthPolicy } from '../auth/authApi';
|
|
6
|
-
export function AuthFactorRequirementCard() {
|
|
6
|
+
export function AuthFactorRequirementCard({ canEdit = true, policy = null }) {
|
|
7
7
|
const { t } = useTranslation();
|
|
8
8
|
const [value, setValue] = useState('1');
|
|
9
9
|
const [busy, setBusy] = useState(false);
|
|
10
10
|
const [error, setError] = useState('');
|
|
11
11
|
const [success, setSuccess] = useState('');
|
|
12
12
|
useEffect(() => {
|
|
13
|
+
if (policy) {
|
|
14
|
+
setValue(String((policy === null || policy === void 0 ? void 0 : policy.required_auth_factor_count) || 1));
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
13
17
|
let active = true;
|
|
14
18
|
(async () => {
|
|
15
19
|
try {
|
|
@@ -25,8 +29,11 @@ export function AuthFactorRequirementCard() {
|
|
|
25
29
|
return () => {
|
|
26
30
|
active = false;
|
|
27
31
|
};
|
|
28
|
-
}, []);
|
|
32
|
+
}, [policy]);
|
|
29
33
|
const handleChange = async (event) => {
|
|
34
|
+
if (!canEdit) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
30
37
|
const nextValue = event.target.value;
|
|
31
38
|
const previous = value;
|
|
32
39
|
setValue(nextValue);
|
|
@@ -45,5 +52,5 @@ export function AuthFactorRequirementCard() {
|
|
|
45
52
|
setBusy(false);
|
|
46
53
|
}
|
|
47
54
|
};
|
|
48
|
-
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.AUTH_FACTOR_TITLE', 'Authentication Requirements') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Auth.AUTH_FACTOR_HINT', 'Define the minimum number of authentication factors required for sign-in.') }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error }), success && _jsx(Alert, { severity: "success", sx: { mb: 2 }, children: success }), _jsx(FormControl, { children: _jsxs(RadioGroup, { value: value, onChange: handleChange, children: [_jsx(FormControlLabel, { value: "1", control: _jsx(Radio, { disabled: busy }), label: t('Auth.ONE_FACTOR_LABEL', 'Allow single-factor authentication') }), _jsx(FormControlLabel, { value: "2", control: _jsx(Radio, { disabled: busy }), label: t('Auth.TWO_FACTOR_LABEL', 'Require two-factor authentication') })] }) })] }));
|
|
55
|
+
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.AUTH_FACTOR_TITLE', 'Authentication Requirements') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Auth.AUTH_FACTOR_HINT', 'Define the minimum number of authentication factors required for sign-in.') }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error }), success && _jsx(Alert, { severity: "success", sx: { mb: 2 }, children: success }), _jsx(FormControl, { children: _jsxs(RadioGroup, { value: value, onChange: handleChange, children: [_jsx(FormControlLabel, { value: "1", control: _jsx(Radio, { disabled: busy || !canEdit }), label: t('Auth.ONE_FACTOR_LABEL', 'Allow single-factor authentication') }), _jsx(FormControlLabel, { value: "2", control: _jsx(Radio, { disabled: busy || !canEdit }), label: t('Auth.TWO_FACTOR_LABEL', 'Require two-factor authentication') })] }) })] }));
|
|
49
56
|
}
|
|
@@ -11,7 +11,7 @@ function clampExpiryDays(value) {
|
|
|
11
11
|
}
|
|
12
12
|
return parsed;
|
|
13
13
|
}
|
|
14
|
-
export function QrSignupValidityManager({ enabled = false, expiryDays = DEFAULT_EXPIRY_DAYS, onPolicyChange, }) {
|
|
14
|
+
export function QrSignupValidityManager({ enabled = false, expiryDays = DEFAULT_EXPIRY_DAYS, onPolicyChange, canEdit = true, }) {
|
|
15
15
|
const { t } = useTranslation();
|
|
16
16
|
const [currentExpiryDays, setCurrentExpiryDays] = useState(String(expiryDays || DEFAULT_EXPIRY_DAYS));
|
|
17
17
|
const [busy, setBusy] = useState(false);
|
|
@@ -44,5 +44,5 @@ export function QrSignupValidityManager({ enabled = false, expiryDays = DEFAULT_
|
|
|
44
44
|
if (!enabled) {
|
|
45
45
|
return null;
|
|
46
46
|
}
|
|
47
|
-
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.SIGNUP_QR_VALIDITY_TITLE', 'QR Signup Validity') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Auth.SIGNUP_QR_VALIDITY_HINT', 'Set the default validity for newly generated QR signup links.') }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error }), success && _jsx(Alert, { severity: "success", sx: { mb: 2 }, children: success }), _jsxs(Stack, { direction: { xs: 'column', sm: 'row' }, spacing: 1.5, alignItems: { sm: 'flex-start' }, children: [_jsx(TextField, { label: t('Auth.SIGNUP_QR_EXPIRY_DAYS_LABEL', 'QR signup validity (days)'), helperText: t('Auth.SIGNUP_QR_EXPIRY_DAYS_HINT', 'Default validity for newly generated QR signup links.'), type: "number", value: currentExpiryDays, onChange: (event) => setCurrentExpiryDays(event.target.value), disabled: busy, sx: { flex: 1 } }), _jsx(Button, { variant: "contained", onClick: handleSave, disabled: busy, sx: { minWidth: 120, mt: { sm: '8px' } }, children: t('Common.SAVE', 'Save') })] })] }));
|
|
47
|
+
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.SIGNUP_QR_VALIDITY_TITLE', 'QR Signup Validity') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Auth.SIGNUP_QR_VALIDITY_HINT', 'Set the default validity for newly generated QR signup links.') }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error }), success && _jsx(Alert, { severity: "success", sx: { mb: 2 }, children: success }), _jsxs(Stack, { direction: { xs: 'column', sm: 'row' }, spacing: 1.5, alignItems: { sm: 'flex-start' }, children: [_jsx(TextField, { label: t('Auth.SIGNUP_QR_EXPIRY_DAYS_LABEL', 'QR signup validity (days)'), helperText: t('Auth.SIGNUP_QR_EXPIRY_DAYS_HINT', 'Default validity for newly generated QR signup links.'), type: "number", value: currentExpiryDays, onChange: (event) => setCurrentExpiryDays(event.target.value), disabled: busy || !canEdit, sx: { flex: 1 } }), _jsx(Button, { variant: "contained", onClick: handleSave, disabled: busy || !canEdit, sx: { minWidth: 120, mt: { sm: '8px' } }, children: t('Common.SAVE', 'Save') })] })] }));
|
|
48
48
|
}
|
|
@@ -13,7 +13,7 @@ const EMPTY_POLICY = {
|
|
|
13
13
|
signup_qr_expiry_days: 90,
|
|
14
14
|
required_auth_factor_count: 1,
|
|
15
15
|
};
|
|
16
|
-
export function RegistrationMethodsManager({ policy: authPolicy, error = '', onPolicyChange, }) {
|
|
16
|
+
export function RegistrationMethodsManager({ policy: authPolicy, error = '', onPolicyChange, canEdit = true, }) {
|
|
17
17
|
const { t } = useTranslation();
|
|
18
18
|
const [policyState, setPolicyState] = useState(EMPTY_POLICY);
|
|
19
19
|
const [busyField, setBusyField] = useState('');
|
|
@@ -49,5 +49,5 @@ export function RegistrationMethodsManager({ policy: authPolicy, error = '', onP
|
|
|
49
49
|
if (!authPolicy && !error) {
|
|
50
50
|
return (_jsx(Box, { sx: { py: 3, display: 'flex', justifyContent: 'center' }, children: _jsx(CircularProgress, { size: 28 }) }));
|
|
51
51
|
}
|
|
52
|
-
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.REGISTRATION_METHODS_TITLE', 'Registration Methods') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Auth.REGISTRATION_METHODS_HINT', 'Choose which signup and invite flows are active for this app.') }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error }), saveError && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: saveError }), success && _jsx(Alert, { severity: "success", sx: { mb: 2 }, children: success }), _jsxs(Stack, { spacing: 1, children: [_jsx(FormControlLabel, { control: (_jsx(Switch, { checked: Boolean(policyState.allow_admin_invite), onChange: toggle('allow_admin_invite'), disabled: Boolean(busyField) })), label: t('Auth.ADMIN_INVITE_LABEL', 'Admin invite') }), _jsx(FormControlLabel, { control: (_jsx(Switch, { checked: Boolean(policyState.allow_self_signup_access_code), onChange: toggle('allow_self_signup_access_code'), disabled: Boolean(busyField) })), label: t('Auth.SIGNUP_ACCESS_CODE_LABEL', 'Self-signup with access code') }), _jsx(FormControlLabel, { control: (_jsx(Switch, { checked: Boolean(policyState.allow_self_signup_open), onChange: toggle('allow_self_signup_open'), disabled: Boolean(busyField) })), label: t('Auth.SIGNUP_OPEN_LABEL', 'Open self-signup') }), _jsx(FormControlLabel, { control: (_jsx(Switch, { checked: Boolean(policyState.allow_self_signup_email_domain), onChange: toggle('allow_self_signup_email_domain'), disabled: Boolean(busyField) })), label: t('Auth.SIGNUP_EMAIL_DOMAIN_LABEL', 'Self-signup by email domain') }), _jsx(FormControlLabel, { control: (_jsx(Switch, { checked: Boolean(policyState.allow_self_signup_qr), onChange: toggle('allow_self_signup_qr'), disabled: Boolean(busyField) })), label: t('Auth.SIGNUP_QR_LABEL', 'Self-signup by QR') })] }), policyState.allow_self_signup_email_domain && !(policyState.allowed_email_domains || []).length && (_jsx(Alert, { severity: "info", sx: { mt: 2 }, children: t('Auth.EMAIL_DOMAIN_CURRENTLY_BLOCKED_HINT', 'Email-domain signup is enabled, but it stays blocked until at least one allowed domain is saved.') }))] }));
|
|
52
|
+
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.REGISTRATION_METHODS_TITLE', 'Registration Methods') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Auth.REGISTRATION_METHODS_HINT', 'Choose which signup and invite flows are active for this app.') }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error }), saveError && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: saveError }), success && _jsx(Alert, { severity: "success", sx: { mb: 2 }, children: success }), _jsxs(Stack, { spacing: 1, children: [_jsx(FormControlLabel, { control: (_jsx(Switch, { checked: Boolean(policyState.allow_admin_invite), onChange: toggle('allow_admin_invite'), disabled: Boolean(busyField) || !canEdit })), label: t('Auth.ADMIN_INVITE_LABEL', 'Admin invite') }), _jsx(FormControlLabel, { control: (_jsx(Switch, { checked: Boolean(policyState.allow_self_signup_access_code), onChange: toggle('allow_self_signup_access_code'), disabled: Boolean(busyField) || !canEdit })), label: t('Auth.SIGNUP_ACCESS_CODE_LABEL', 'Self-signup with access code') }), _jsx(FormControlLabel, { control: (_jsx(Switch, { checked: Boolean(policyState.allow_self_signup_open), onChange: toggle('allow_self_signup_open'), disabled: Boolean(busyField) || !canEdit })), label: t('Auth.SIGNUP_OPEN_LABEL', 'Open self-signup') }), _jsx(FormControlLabel, { control: (_jsx(Switch, { checked: Boolean(policyState.allow_self_signup_email_domain), onChange: toggle('allow_self_signup_email_domain'), disabled: Boolean(busyField) || !canEdit })), label: t('Auth.SIGNUP_EMAIL_DOMAIN_LABEL', 'Self-signup by email domain') }), _jsx(FormControlLabel, { control: (_jsx(Switch, { checked: Boolean(policyState.allow_self_signup_qr), onChange: toggle('allow_self_signup_qr'), disabled: Boolean(busyField) || !canEdit })), label: t('Auth.SIGNUP_QR_LABEL', 'Self-signup by QR') })] }), policyState.allow_self_signup_email_domain && !(policyState.allowed_email_domains || []).length && (_jsx(Alert, { severity: "info", sx: { mt: 2 }, children: t('Auth.EMAIL_DOMAIN_CURRENTLY_BLOCKED_HINT', 'Email-domain signup is enabled, but it stays blocked until at least one allowed domain is saved.') }))] }));
|
|
53
53
|
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// src/components/SupportRecoveryRequestsTab.jsx
|
|
3
|
-
import React, { useEffect, useState } from 'react';
|
|
4
|
-
import { Box,
|
|
3
|
+
import React, { useContext, useEffect, useState } from 'react';
|
|
4
|
+
import { Alert, Autocomplete, Box, Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Paper, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Typography, } from '@mui/material';
|
|
5
|
+
import CloseIcon from '@mui/icons-material/Close';
|
|
5
6
|
import { useTranslation } from 'react-i18next';
|
|
6
|
-
import {
|
|
7
|
+
import { AuthContext } from '../auth/AuthContext';
|
|
8
|
+
import { approveRecoveryRequest, fetchRecoveryRequests, fetchUsersList, rejectRecoveryRequest, updateUserSupportStatus, } from '../auth/authApi';
|
|
7
9
|
export function SupportRecoveryRequestsTab() {
|
|
8
10
|
const { t } = useTranslation();
|
|
11
|
+
const { user } = useContext(AuthContext);
|
|
12
|
+
const canManageAgents = Boolean((user === null || user === void 0 ? void 0 : user.is_superuser) || (user === null || user === void 0 ? void 0 : user.can_manage_support_agents));
|
|
9
13
|
const [requests, setRequests] = useState([]);
|
|
10
14
|
const [loading, setLoading] = useState(true);
|
|
11
15
|
const [errorKey, setErrorKey] = useState(null);
|
|
@@ -13,6 +17,11 @@ export function SupportRecoveryRequestsTab() {
|
|
|
13
17
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
14
18
|
const [dialogNote, setDialogNote] = useState('');
|
|
15
19
|
const [selectedRequest, setSelectedRequest] = useState(null);
|
|
20
|
+
const [allUsers, setAllUsers] = useState([]);
|
|
21
|
+
const [agentLoading, setAgentLoading] = useState(false);
|
|
22
|
+
const [agentErrorKey, setAgentErrorKey] = useState(null);
|
|
23
|
+
const [selectedAgentCandidate, setSelectedAgentCandidate] = useState(null);
|
|
24
|
+
const [agentActionUserId, setAgentActionUserId] = useState(null);
|
|
16
25
|
const loadRequests = async () => {
|
|
17
26
|
setLoading(true);
|
|
18
27
|
setErrorKey(null);
|
|
@@ -27,10 +36,39 @@ export function SupportRecoveryRequestsTab() {
|
|
|
27
36
|
setLoading(false);
|
|
28
37
|
}
|
|
29
38
|
};
|
|
39
|
+
const loadUsers = async () => {
|
|
40
|
+
if (!canManageAgents) {
|
|
41
|
+
setAllUsers([]);
|
|
42
|
+
setAgentErrorKey(null);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
setAgentLoading(true);
|
|
46
|
+
setAgentErrorKey(null);
|
|
47
|
+
try {
|
|
48
|
+
const data = await fetchUsersList();
|
|
49
|
+
const list = Array.isArray(data) ? data : (Array.isArray(data === null || data === void 0 ? void 0 : data.results) ? data.results : []);
|
|
50
|
+
list.sort((a, b) => {
|
|
51
|
+
const emailA = String((a === null || a === void 0 ? void 0 : a.email) || '').toLocaleLowerCase();
|
|
52
|
+
const emailB = String((b === null || b === void 0 ? void 0 : b.email) || '').toLocaleLowerCase();
|
|
53
|
+
return emailA.localeCompare(emailB);
|
|
54
|
+
});
|
|
55
|
+
setAllUsers(list);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
setAgentErrorKey(err.code || 'Auth.USER_LIST_FAILED');
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
setAgentLoading(false);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
30
64
|
useEffect(() => {
|
|
31
65
|
loadRequests();
|
|
32
66
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
33
67
|
}, [statusFilter]);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
loadUsers();
|
|
70
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
71
|
+
}, [canManageAgents]);
|
|
34
72
|
const openDialog = (req) => {
|
|
35
73
|
setSelectedRequest(req);
|
|
36
74
|
setDialogNote('');
|
|
@@ -67,10 +105,62 @@ export function SupportRecoveryRequestsTab() {
|
|
|
67
105
|
setErrorKey(err.code || 'Support.RECOVERY_REQUEST_REJECT_FAILED');
|
|
68
106
|
}
|
|
69
107
|
};
|
|
108
|
+
const getUserDisplayName = (entry) => {
|
|
109
|
+
if ((entry === null || entry === void 0 ? void 0 : entry.first_name) || (entry === null || entry === void 0 ? void 0 : entry.last_name)) {
|
|
110
|
+
return `${entry.first_name || ''} ${entry.last_name || ''}`.trim();
|
|
111
|
+
}
|
|
112
|
+
return (entry === null || entry === void 0 ? void 0 : entry.username) || '';
|
|
113
|
+
};
|
|
114
|
+
const getUserOptionLabel = (entry) => {
|
|
115
|
+
if (!entry)
|
|
116
|
+
return '';
|
|
117
|
+
const name = getUserDisplayName(entry);
|
|
118
|
+
if (name && (entry === null || entry === void 0 ? void 0 : entry.email))
|
|
119
|
+
return `${name} (${entry.email})`;
|
|
120
|
+
return (entry === null || entry === void 0 ? void 0 : entry.email) || name || `#${(entry === null || entry === void 0 ? void 0 : entry.id) || ''}`;
|
|
121
|
+
};
|
|
122
|
+
const supportAgents = allUsers.filter((entry) => Boolean(entry === null || entry === void 0 ? void 0 : entry.is_support_agent));
|
|
123
|
+
const availableAgentCandidates = allUsers.filter((entry) => !(entry === null || entry === void 0 ? void 0 : entry.is_support_agent));
|
|
124
|
+
const handleAddAgent = async () => {
|
|
125
|
+
if (!selectedAgentCandidate)
|
|
126
|
+
return;
|
|
127
|
+
setAgentActionUserId(selectedAgentCandidate.id);
|
|
128
|
+
setAgentErrorKey(null);
|
|
129
|
+
try {
|
|
130
|
+
await updateUserSupportStatus(selectedAgentCandidate.id, true);
|
|
131
|
+
setSelectedAgentCandidate(null);
|
|
132
|
+
await loadUsers();
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
setAgentErrorKey(err.code || 'Auth.USER_SUPPORT_UPDATE_FAILED');
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
setAgentActionUserId(null);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const handleRemoveAgent = async (agentUser) => {
|
|
142
|
+
if (!(agentUser === null || agentUser === void 0 ? void 0 : agentUser.id))
|
|
143
|
+
return;
|
|
144
|
+
setAgentActionUserId(agentUser.id);
|
|
145
|
+
setAgentErrorKey(null);
|
|
146
|
+
try {
|
|
147
|
+
await updateUserSupportStatus(agentUser.id, false);
|
|
148
|
+
if ((selectedAgentCandidate === null || selectedAgentCandidate === void 0 ? void 0 : selectedAgentCandidate.id) === agentUser.id) {
|
|
149
|
+
setSelectedAgentCandidate(null);
|
|
150
|
+
}
|
|
151
|
+
await loadUsers();
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
setAgentErrorKey(err.code || 'Auth.USER_SUPPORT_UPDATE_FAILED');
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
setAgentActionUserId(null);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
70
160
|
if (loading) {
|
|
71
161
|
return (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', mt: 4 }, children: _jsx(CircularProgress, {}) }));
|
|
72
162
|
}
|
|
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_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
|
|
163
|
+
return (_jsxs(Box, { children: [canManageAgents && (_jsxs(Paper, { variant: "outlined", sx: { p: 2, mb: 3, borderRadius: 2 }, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Support.AGENTS_TITLE', 'Support agents') }), _jsx(Typography, { variant: "body2", sx: { mb: 2 }, children: t('Support.AGENTS_DESCRIPTION', 'Assign which users are allowed to handle support requests.') }), agentErrorKey && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(agentErrorKey) })), _jsxs(Stack, { direction: { xs: 'column', md: 'row' }, spacing: 2, sx: { mb: 2, alignItems: { md: 'flex-start' } }, children: [_jsx(Autocomplete, { fullWidth: true, options: availableAgentCandidates, value: selectedAgentCandidate, loading: agentLoading, onChange: (_event, value) => setSelectedAgentCandidate(value), isOptionEqualToValue: (option, value) => option.id === value.id, getOptionLabel: getUserOptionLabel, renderInput: (params) => (_jsx(TextField, Object.assign({}, params, { label: t('Support.AGENTS_SELECT_LABEL', 'Select user'), placeholder: t('Support.AGENTS_SELECT_PLACEHOLDER', 'Choose a user') }))) }), _jsx(Button, { variant: "contained", onClick: handleAddAgent, disabled: !selectedAgentCandidate || agentLoading || Boolean(agentActionUserId), sx: { minWidth: 160 }, children: t('Support.AGENTS_ADD', 'Add as agent') })] }), agentLoading ? (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', py: 3 }, children: _jsx(CircularProgress, { size: 24 }) })) : supportAgents.length === 0 ? (_jsx(Typography, { variant: "body2", children: t('Support.AGENTS_EMPTY', 'No support agents assigned yet.') })) : (_jsx(TableContainer, { component: Paper, variant: "outlined", children: _jsxs(Table, { size: "small", children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { children: t('Profile.NAME_LABEL', 'Name') }), _jsx(TableCell, { children: t('Auth.EMAIL_LABEL', 'Email') }), _jsx(TableCell, { align: "right", children: t('Common.ACTIONS', 'Actions') })] }) }), _jsx(TableBody, { children: supportAgents.map((agentUser) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: getUserDisplayName(agentUser) || '-' }), _jsx(TableCell, { children: agentUser.email || '-' }), _jsx(TableCell, { align: "right", children: _jsx(IconButton, { size: "small", color: "error", onClick: () => handleRemoveAgent(agentUser), disabled: agentActionUserId === agentUser.id, "aria-label": t('Support.AGENTS_REMOVE', 'Remove support agent'), children: _jsx(CloseIcon, { fontSize: "small" }) }) })] }, agentUser.id))) })] }) }))] })), _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_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
|
|
74
164
|
? new Date(req.created_at).toLocaleString()
|
|
75
165
|
: '-' }), _jsx(TableCell, { children: req.user_email || req.user }), _jsx(TableCell, { children: t(`Support.RECOVERY_STATUS_${req.status}`, 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_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
166
|
? new Date(selectedRequest.created_at).toLocaleString()
|
|
@@ -4,7 +4,7 @@ import { Box, Typography, FormControl, Select, MenuItem, Button, Tooltip, Circul
|
|
|
4
4
|
import { DataGrid } from '@mui/x-data-grid';
|
|
5
5
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
|
-
import { fetchUsersList, deleteUser, updateUserRole
|
|
7
|
+
import { fetchUsersList, deleteUser, updateUserRole } from '../auth/authApi';
|
|
8
8
|
const DEFAULT_ROLES = ['none', 'student', 'teacher', 'admin'];
|
|
9
9
|
export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraColumns = [], extraRowActions = [], extraContext = null, refreshTrigger = 0, canEditUser, }) {
|
|
10
10
|
const { t } = useTranslation();
|
|
@@ -60,15 +60,6 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
60
60
|
alert(t(err.code || 'Auth.USER_ROLE_UPDATE_FAILED'));
|
|
61
61
|
}
|
|
62
62
|
};
|
|
63
|
-
const handleToggleSupporter = async (userId, newValue) => {
|
|
64
|
-
try {
|
|
65
|
-
await updateUserSupportStatus(userId, newValue);
|
|
66
|
-
await loadUsers();
|
|
67
|
-
}
|
|
68
|
-
catch (err) {
|
|
69
|
-
alert(t(err.code || 'Auth.USER_UPDATE_FAILED'));
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
63
|
const defaultCanEdit = (targetUser) => {
|
|
73
64
|
if (!currentUser)
|
|
74
65
|
return false;
|
|
@@ -142,6 +133,8 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
142
133
|
return String(value).toLocaleLowerCase();
|
|
143
134
|
};
|
|
144
135
|
const getSearchBucketForUser = (user) => {
|
|
136
|
+
var _a;
|
|
137
|
+
const hasSuccessfulLogin = Boolean((_a = user === null || user === void 0 ? void 0 : user.successful_login) !== null && _a !== void 0 ? _a : user === null || user === void 0 ? void 0 : user.last_login);
|
|
145
138
|
const baseValues = [
|
|
146
139
|
user === null || user === void 0 ? void 0 : user.id,
|
|
147
140
|
user === null || user === void 0 ? void 0 : user.email,
|
|
@@ -151,7 +144,7 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
151
144
|
getUserDisplayName(user),
|
|
152
145
|
user === null || user === void 0 ? void 0 : user.role,
|
|
153
146
|
user === null || user === void 0 ? void 0 : user.language,
|
|
154
|
-
|
|
147
|
+
hasSuccessfulLogin ? t('Common.YES', 'Yes') : t('Common.NO', 'No'),
|
|
155
148
|
];
|
|
156
149
|
const extraValues = visibleExtraColumns.map((column) => getExtraColumnSearchValue(column, user));
|
|
157
150
|
return [...baseValues, ...extraValues].map((value) => toSearchText(value)).filter(Boolean);
|
|
@@ -202,25 +195,27 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
202
195
|
valueGetter: (_value, row) => getUserDisplayName(row),
|
|
203
196
|
},
|
|
204
197
|
{
|
|
205
|
-
field: '
|
|
206
|
-
headerName: t('UserList.
|
|
198
|
+
field: 'successful_login',
|
|
199
|
+
headerName: t('UserList.SUCCESSFUL_LOGIN', 'Successful Login'),
|
|
207
200
|
minWidth: 180,
|
|
208
201
|
flex: 0.8,
|
|
209
|
-
valueGetter: (_value, row) => (row === null || row === void 0 ? void 0 : row.
|
|
202
|
+
valueGetter: (_value, row) => { var _a; return Boolean((_a = row === null || row === void 0 ? void 0 : row.successful_login) !== null && _a !== void 0 ? _a : row === null || row === void 0 ? void 0 : row.last_login); },
|
|
210
203
|
renderCell: (params) => {
|
|
211
|
-
|
|
212
|
-
return (_jsx(
|
|
204
|
+
var _a, _b, _c;
|
|
205
|
+
return (_jsx(Typography, { variant: "body2", children: Boolean((_b = (_a = params.row) === null || _a === void 0 ? void 0 : _a.successful_login) !== null && _b !== void 0 ? _b : (_c = params.row) === null || _c === void 0 ? void 0 : _c.last_login)
|
|
206
|
+
? t('Common.YES', 'Yes')
|
|
207
|
+
: t('Common.NO', 'No') }));
|
|
213
208
|
},
|
|
214
209
|
},
|
|
215
210
|
{
|
|
216
|
-
field: '
|
|
217
|
-
headerName: t('UserList.
|
|
218
|
-
minWidth:
|
|
211
|
+
field: 'role',
|
|
212
|
+
headerName: t('UserList.ROLE', 'Role'),
|
|
213
|
+
minWidth: 180,
|
|
219
214
|
flex: 0.8,
|
|
220
|
-
valueGetter: (_value, row) =>
|
|
215
|
+
valueGetter: (_value, row) => (row === null || row === void 0 ? void 0 : row.role) || 'none',
|
|
221
216
|
renderCell: (params) => {
|
|
222
217
|
const user = params.row;
|
|
223
|
-
return (_jsx(FormControl, { size: "small", fullWidth: true, sx: controlSx, disabled: !canEdit(user), children:
|
|
218
|
+
return (_jsx(FormControl, { size: "small", fullWidth: true, sx: controlSx, disabled: !canEdit(user), children: _jsx(Select, { value: user.role || 'none', onChange: (event) => handleChangeRole(user.id, event.target.value), variant: "outlined", children: roles.map((role) => _jsx(MenuItem, { value: role, children: role }, role)) }) }));
|
|
224
219
|
},
|
|
225
220
|
},
|
|
226
221
|
];
|
|
@@ -40,6 +40,13 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
40
40
|
const perms = (user === null || user === void 0 ? void 0 : user.ui_permissions) || {};
|
|
41
41
|
// NEU: Superuser-Flag prüfen
|
|
42
42
|
const isSuperUser = (user === null || user === void 0 ? void 0 : user.is_superuser) || false;
|
|
43
|
+
const canViewUsers = Boolean(isSuperUser || perms.can_view_users);
|
|
44
|
+
const canViewInvite = Boolean(isSuperUser || perms.can_view_invite || perms.can_invite);
|
|
45
|
+
const canViewAuthPolicy = Boolean(isSuperUser || perms.can_view_auth_policy);
|
|
46
|
+
const canWriteAuthPolicy = Boolean(isSuperUser || perms.can_write_auth_policy);
|
|
47
|
+
const canSendInvites = Boolean(isSuperUser || perms.can_send_invites);
|
|
48
|
+
const canManageAccessCodes = Boolean(isSuperUser || perms.can_manage_access_codes);
|
|
49
|
+
const canManageSignupQr = Boolean(isSuperUser || perms.can_manage_signup_qr);
|
|
43
50
|
const handleTabChange = (_event, newValue) => {
|
|
44
51
|
setSearchParams({ tab: newValue });
|
|
45
52
|
};
|
|
@@ -49,7 +56,7 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
49
56
|
};
|
|
50
57
|
useEffect(() => {
|
|
51
58
|
let active = true;
|
|
52
|
-
const canLoadPolicy = Boolean(user) && (
|
|
59
|
+
const canLoadPolicy = Boolean(user) && (canViewAuthPolicy || canManageSignupQr || canManageAccessCodes);
|
|
53
60
|
if (!canLoadPolicy) {
|
|
54
61
|
setAuthPolicy(null);
|
|
55
62
|
setAuthPolicyError('');
|
|
@@ -74,7 +81,7 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
74
81
|
return () => {
|
|
75
82
|
active = false;
|
|
76
83
|
};
|
|
77
|
-
}, [user,
|
|
84
|
+
}, [user, canViewAuthPolicy, canManageSignupQr, canManageAccessCodes, t]);
|
|
78
85
|
// 3. Dynamic Tabs (angepasst für Superuser)
|
|
79
86
|
const tabs = useMemo(() => {
|
|
80
87
|
if (!user)
|
|
@@ -85,10 +92,10 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
85
92
|
{ value: 'security', label: t('Account.TAB_SECURITY', 'Security') },
|
|
86
93
|
];
|
|
87
94
|
// Logik: Superuser darf ALLES, sonst gelten die spezifischen Permissions
|
|
88
|
-
if (
|
|
95
|
+
if (canViewUsers) {
|
|
89
96
|
list.push({ value: 'users', label: t('Account.TAB_USERS', 'Users') });
|
|
90
97
|
}
|
|
91
|
-
if (
|
|
98
|
+
if (canViewInvite) {
|
|
92
99
|
list.push({ value: 'invite', label: t('Account.TAB_INVITE', 'Invite') });
|
|
93
100
|
}
|
|
94
101
|
if (isSuperUser || perms.can_view_support) {
|
|
@@ -106,7 +113,7 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
106
113
|
}
|
|
107
114
|
});
|
|
108
115
|
return list;
|
|
109
|
-
}, [user, perms, t, isSuperUser, extraTabs]);
|
|
116
|
+
}, [user, perms, t, isSuperUser, extraTabs, canViewUsers, canViewInvite]);
|
|
110
117
|
// 4. Loading & Auth Checks
|
|
111
118
|
if (loading) {
|
|
112
119
|
return (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', mt: 10 }, children: _jsx(CircularProgress, {}) }));
|
|
@@ -122,5 +129,5 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
122
129
|
const activeExtraTab = builtInTabValues.has(safeTab)
|
|
123
130
|
? null
|
|
124
131
|
: extraTabs.find((tab) => tab.value === safeTab);
|
|
125
|
-
return (_jsxs(WidePage, { title: t('Account.TITLE', 'Account & Administration'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('Account.PAGE_TITLE', 'Account'), " \u2013 ", user.email] }) }), _jsx(Tabs, { value: safeTab, onChange: handleTabChange, variant: "scrollable", scrollButtons: "auto", sx: { mb: 3, borderBottom: 1, borderColor: 'divider' }, children: tabs.map((tab) => (_jsx(Tab, { label: tab.label, value: tab.value }, tab.value))) }), safeTab === 'profile' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(ProfileComponent, { onSubmit: handleProfileSubmit, showName: true, showPrivacy: true, showCookies: true }) })), safeTab === 'security' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SecurityComponent, { fromRecovery: fromRecovery, fromWeakLogin: fromWeakLogin }) })), safeTab === 'users' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(UserListComponent, { roles: activeRoles, currentUser: user, extraColumns: userListExtraColumns, extraRowActions: userListExtraRowActions, extraContext: userListExtraContext, refreshTrigger: userListRefreshTrigger, canEditUser: userListCanEditUser }) })), safeTab === 'invite' && (_jsx(Box, { sx: { mt: 2 }, children: _jsxs(Stack, { spacing: 2.5, children: [
|
|
132
|
+
return (_jsxs(WidePage, { title: t('Account.TITLE', 'Account & Administration'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('Account.PAGE_TITLE', 'Account'), " \u2013 ", user.email] }) }), _jsx(Tabs, { value: safeTab, onChange: handleTabChange, variant: "scrollable", scrollButtons: "auto", sx: { mb: 3, borderBottom: 1, borderColor: 'divider' }, children: tabs.map((tab) => (_jsx(Tab, { label: tab.label, value: tab.value }, tab.value))) }), safeTab === 'profile' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(ProfileComponent, { onSubmit: handleProfileSubmit, showName: true, showPrivacy: true, showCookies: true }) })), safeTab === 'security' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SecurityComponent, { fromRecovery: fromRecovery, fromWeakLogin: fromWeakLogin }) })), safeTab === 'users' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(UserListComponent, { roles: activeRoles, currentUser: user, extraColumns: userListExtraColumns, extraRowActions: userListExtraRowActions, extraContext: userListExtraContext, refreshTrigger: userListRefreshTrigger, canEditUser: userListCanEditUser }) })), safeTab === 'invite' && (_jsx(Box, { sx: { mt: 2 }, children: _jsxs(Stack, { spacing: 2.5, children: [canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(AuthFactorRequirementCard, { canEdit: canWriteAuthPolicy, policy: authPolicy }) })), canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(RegistrationMethodsManager, { policy: authPolicy, error: authPolicyError, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canSendInvites && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(UserInviteComponent, {}) })), canManageAccessCodes && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_access_code) && (_jsxs(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.ACCESS_CODE_MANAGER_TITLE', 'Access Codes') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Account.ACCESS_CODES_HINT', 'Manage access codes for self-registration.') }), _jsx(AccessCodeManager, {})] })), canSendInvites && showBulkInviteCsvTab && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(BulkInviteCsvTab, Object.assign({}, bulkInviteCsvProps)) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(AllowedEmailDomainsManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain), domains: (authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allowed_email_domains) || [], onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(QrSignupValidityManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canManageSignupQr && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(QrSignupManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days }) }))] }) })), safeTab === 'support' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SupportRecoveryRequestsTab, {}) })), activeExtraTab && (_jsx(Box, { sx: { mt: 2 }, children: (_a = activeExtraTab.render) === null || _a === void 0 ? void 0 : _a.call(activeExtraTab, { user, perms, isSuperUser, t }) }))] }));
|
|
126
133
|
}
|
package/package.json
CHANGED
package/src/auth/AuthContext.jsx
CHANGED
|
@@ -64,6 +64,7 @@ export const AuthProvider = ({ children }) => {
|
|
|
64
64
|
can_manage_support_agents: Boolean(data?.can_manage_support_agents),
|
|
65
65
|
can_manage: Boolean(data?.can_manage),
|
|
66
66
|
is_active: data?.is_active,
|
|
67
|
+
successful_login: Boolean(data?.successful_login ?? data?.last_login),
|
|
67
68
|
last_login: data?.last_login,
|
|
68
69
|
date_joined: data?.date_joined,
|
|
69
70
|
};
|
|
@@ -13,6 +13,7 @@ export function AllowedEmailDomainsManager({
|
|
|
13
13
|
domains = [],
|
|
14
14
|
enabled = false,
|
|
15
15
|
onPolicyChange,
|
|
16
|
+
canEdit = true,
|
|
16
17
|
}) {
|
|
17
18
|
const { t } = useTranslation();
|
|
18
19
|
const [domainsText, setDomainsText] = useState('');
|
|
@@ -76,10 +77,10 @@ export function AllowedEmailDomainsManager({
|
|
|
76
77
|
fullWidth
|
|
77
78
|
value={domainsText}
|
|
78
79
|
onChange={(event) => setDomainsText(event.target.value)}
|
|
79
|
-
disabled={busy}
|
|
80
|
+
disabled={busy || !canEdit}
|
|
80
81
|
/>
|
|
81
82
|
|
|
82
|
-
<Button variant="contained" sx={{ mt: 2 }} onClick={handleSave} disabled={busy}>
|
|
83
|
+
<Button variant="contained" sx={{ mt: 2 }} onClick={handleSave} disabled={busy || !canEdit}>
|
|
83
84
|
{t('Common.SAVE', 'Save')}
|
|
84
85
|
</Button>
|
|
85
86
|
</Box>
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import { useTranslation } from 'react-i18next';
|
|
12
12
|
import { fetchAuthPolicy, updateAuthPolicy } from '../auth/authApi';
|
|
13
13
|
|
|
14
|
-
export function AuthFactorRequirementCard() {
|
|
14
|
+
export function AuthFactorRequirementCard({ canEdit = true, policy = null }) {
|
|
15
15
|
const { t } = useTranslation();
|
|
16
16
|
const [value, setValue] = useState('1');
|
|
17
17
|
const [busy, setBusy] = useState(false);
|
|
@@ -19,6 +19,10 @@ export function AuthFactorRequirementCard() {
|
|
|
19
19
|
const [success, setSuccess] = useState('');
|
|
20
20
|
|
|
21
21
|
useEffect(() => {
|
|
22
|
+
if (policy) {
|
|
23
|
+
setValue(String(policy?.required_auth_factor_count || 1));
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
22
26
|
let active = true;
|
|
23
27
|
(async () => {
|
|
24
28
|
try {
|
|
@@ -33,9 +37,12 @@ export function AuthFactorRequirementCard() {
|
|
|
33
37
|
return () => {
|
|
34
38
|
active = false;
|
|
35
39
|
};
|
|
36
|
-
}, []);
|
|
40
|
+
}, [policy]);
|
|
37
41
|
|
|
38
42
|
const handleChange = async (event) => {
|
|
43
|
+
if (!canEdit) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
39
46
|
const nextValue = event.target.value;
|
|
40
47
|
const previous = value;
|
|
41
48
|
setValue(nextValue);
|
|
@@ -70,12 +77,12 @@ export function AuthFactorRequirementCard() {
|
|
|
70
77
|
<RadioGroup value={value} onChange={handleChange}>
|
|
71
78
|
<FormControlLabel
|
|
72
79
|
value="1"
|
|
73
|
-
control={<Radio disabled={busy} />}
|
|
80
|
+
control={<Radio disabled={busy || !canEdit} />}
|
|
74
81
|
label={t('Auth.ONE_FACTOR_LABEL', 'Allow single-factor authentication')}
|
|
75
82
|
/>
|
|
76
83
|
<FormControlLabel
|
|
77
84
|
value="2"
|
|
78
|
-
control={<Radio disabled={busy} />}
|
|
85
|
+
control={<Radio disabled={busy || !canEdit} />}
|
|
79
86
|
label={t('Auth.TWO_FACTOR_LABEL', 'Require two-factor authentication')}
|
|
80
87
|
/>
|
|
81
88
|
</RadioGroup>
|
|
@@ -24,6 +24,7 @@ export function QrSignupValidityManager({
|
|
|
24
24
|
enabled = false,
|
|
25
25
|
expiryDays = DEFAULT_EXPIRY_DAYS,
|
|
26
26
|
onPolicyChange,
|
|
27
|
+
canEdit = true,
|
|
27
28
|
}) {
|
|
28
29
|
const { t } = useTranslation();
|
|
29
30
|
const [currentExpiryDays, setCurrentExpiryDays] = useState(String(expiryDays || DEFAULT_EXPIRY_DAYS));
|
|
@@ -83,13 +84,13 @@ export function QrSignupValidityManager({
|
|
|
83
84
|
type="number"
|
|
84
85
|
value={currentExpiryDays}
|
|
85
86
|
onChange={(event) => setCurrentExpiryDays(event.target.value)}
|
|
86
|
-
disabled={busy}
|
|
87
|
+
disabled={busy || !canEdit}
|
|
87
88
|
sx={{ flex: 1 }}
|
|
88
89
|
/>
|
|
89
90
|
<Button
|
|
90
91
|
variant="contained"
|
|
91
92
|
onClick={handleSave}
|
|
92
|
-
disabled={busy}
|
|
93
|
+
disabled={busy || !canEdit}
|
|
93
94
|
sx={{ minWidth: 120, mt: { sm: '8px' } }}
|
|
94
95
|
>
|
|
95
96
|
{t('Common.SAVE', 'Save')}
|
|
@@ -26,6 +26,7 @@ export function RegistrationMethodsManager({
|
|
|
26
26
|
policy: authPolicy,
|
|
27
27
|
error = '',
|
|
28
28
|
onPolicyChange,
|
|
29
|
+
canEdit = true,
|
|
29
30
|
}) {
|
|
30
31
|
const { t } = useTranslation();
|
|
31
32
|
const [policyState, setPolicyState] = useState(EMPTY_POLICY);
|
|
@@ -85,7 +86,7 @@ export function RegistrationMethodsManager({
|
|
|
85
86
|
<Switch
|
|
86
87
|
checked={Boolean(policyState.allow_admin_invite)}
|
|
87
88
|
onChange={toggle('allow_admin_invite')}
|
|
88
|
-
disabled={Boolean(busyField)}
|
|
89
|
+
disabled={Boolean(busyField) || !canEdit}
|
|
89
90
|
/>
|
|
90
91
|
)}
|
|
91
92
|
label={t('Auth.ADMIN_INVITE_LABEL', 'Admin invite')}
|
|
@@ -95,7 +96,7 @@ export function RegistrationMethodsManager({
|
|
|
95
96
|
<Switch
|
|
96
97
|
checked={Boolean(policyState.allow_self_signup_access_code)}
|
|
97
98
|
onChange={toggle('allow_self_signup_access_code')}
|
|
98
|
-
disabled={Boolean(busyField)}
|
|
99
|
+
disabled={Boolean(busyField) || !canEdit}
|
|
99
100
|
/>
|
|
100
101
|
)}
|
|
101
102
|
label={t('Auth.SIGNUP_ACCESS_CODE_LABEL', 'Self-signup with access code')}
|
|
@@ -105,7 +106,7 @@ export function RegistrationMethodsManager({
|
|
|
105
106
|
<Switch
|
|
106
107
|
checked={Boolean(policyState.allow_self_signup_open)}
|
|
107
108
|
onChange={toggle('allow_self_signup_open')}
|
|
108
|
-
disabled={Boolean(busyField)}
|
|
109
|
+
disabled={Boolean(busyField) || !canEdit}
|
|
109
110
|
/>
|
|
110
111
|
)}
|
|
111
112
|
label={t('Auth.SIGNUP_OPEN_LABEL', 'Open self-signup')}
|
|
@@ -115,7 +116,7 @@ export function RegistrationMethodsManager({
|
|
|
115
116
|
<Switch
|
|
116
117
|
checked={Boolean(policyState.allow_self_signup_email_domain)}
|
|
117
118
|
onChange={toggle('allow_self_signup_email_domain')}
|
|
118
|
-
disabled={Boolean(busyField)}
|
|
119
|
+
disabled={Boolean(busyField) || !canEdit}
|
|
119
120
|
/>
|
|
120
121
|
)}
|
|
121
122
|
label={t('Auth.SIGNUP_EMAIL_DOMAIN_LABEL', 'Self-signup by email domain')}
|
|
@@ -125,7 +126,7 @@ export function RegistrationMethodsManager({
|
|
|
125
126
|
<Switch
|
|
126
127
|
checked={Boolean(policyState.allow_self_signup_qr)}
|
|
127
128
|
onChange={toggle('allow_self_signup_qr')}
|
|
128
|
-
disabled={Boolean(busyField)}
|
|
129
|
+
disabled={Boolean(busyField) || !canEdit}
|
|
129
130
|
/>
|
|
130
131
|
)}
|
|
131
132
|
label={t('Auth.SIGNUP_QR_LABEL', 'Self-signup by QR')}
|
|
@@ -1,30 +1,42 @@
|
|
|
1
1
|
// src/components/SupportRecoveryRequestsTab.jsx
|
|
2
|
-
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import React, { useContext, useEffect, useState } from 'react';
|
|
3
3
|
import {
|
|
4
|
+
Alert,
|
|
5
|
+
Autocomplete,
|
|
4
6
|
Box,
|
|
5
|
-
Typography,
|
|
6
|
-
Table,
|
|
7
|
-
TableHead,
|
|
8
|
-
TableBody,
|
|
9
|
-
TableRow,
|
|
10
|
-
TableCell,
|
|
11
|
-
TableContainer,
|
|
12
|
-
Paper,
|
|
13
7
|
Button,
|
|
14
8
|
CircularProgress,
|
|
15
|
-
Alert,
|
|
16
|
-
Stack,
|
|
17
9
|
Dialog,
|
|
18
|
-
DialogTitle,
|
|
19
|
-
DialogContent,
|
|
20
10
|
DialogActions,
|
|
11
|
+
DialogContent,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
IconButton,
|
|
14
|
+
Paper,
|
|
15
|
+
Stack,
|
|
16
|
+
Table,
|
|
17
|
+
TableBody,
|
|
18
|
+
TableCell,
|
|
19
|
+
TableContainer,
|
|
20
|
+
TableHead,
|
|
21
|
+
TableRow,
|
|
21
22
|
TextField,
|
|
23
|
+
Typography,
|
|
22
24
|
} from '@mui/material';
|
|
25
|
+
import CloseIcon from '@mui/icons-material/Close';
|
|
23
26
|
import { useTranslation } from 'react-i18next';
|
|
24
|
-
import {
|
|
27
|
+
import { AuthContext } from '../auth/AuthContext';
|
|
28
|
+
import {
|
|
29
|
+
approveRecoveryRequest,
|
|
30
|
+
fetchRecoveryRequests,
|
|
31
|
+
fetchUsersList,
|
|
32
|
+
rejectRecoveryRequest,
|
|
33
|
+
updateUserSupportStatus,
|
|
34
|
+
} from '../auth/authApi';
|
|
25
35
|
|
|
26
36
|
export function SupportRecoveryRequestsTab() {
|
|
27
37
|
const { t } = useTranslation();
|
|
38
|
+
const { user } = useContext(AuthContext);
|
|
39
|
+
const canManageAgents = Boolean(user?.is_superuser || user?.can_manage_support_agents);
|
|
28
40
|
|
|
29
41
|
const [requests, setRequests] = useState([]);
|
|
30
42
|
const [loading, setLoading] = useState(true);
|
|
@@ -34,6 +46,11 @@ export function SupportRecoveryRequestsTab() {
|
|
|
34
46
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
35
47
|
const [dialogNote, setDialogNote] = useState('');
|
|
36
48
|
const [selectedRequest, setSelectedRequest] = useState(null);
|
|
49
|
+
const [allUsers, setAllUsers] = useState([]);
|
|
50
|
+
const [agentLoading, setAgentLoading] = useState(false);
|
|
51
|
+
const [agentErrorKey, setAgentErrorKey] = useState(null);
|
|
52
|
+
const [selectedAgentCandidate, setSelectedAgentCandidate] = useState(null);
|
|
53
|
+
const [agentActionUserId, setAgentActionUserId] = useState(null);
|
|
37
54
|
|
|
38
55
|
const loadRequests = async () => {
|
|
39
56
|
setLoading(true);
|
|
@@ -48,11 +65,41 @@ export function SupportRecoveryRequestsTab() {
|
|
|
48
65
|
}
|
|
49
66
|
};
|
|
50
67
|
|
|
68
|
+
const loadUsers = async () => {
|
|
69
|
+
if (!canManageAgents) {
|
|
70
|
+
setAllUsers([]);
|
|
71
|
+
setAgentErrorKey(null);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
setAgentLoading(true);
|
|
76
|
+
setAgentErrorKey(null);
|
|
77
|
+
try {
|
|
78
|
+
const data = await fetchUsersList();
|
|
79
|
+
const list = Array.isArray(data) ? data : (Array.isArray(data?.results) ? data.results : []);
|
|
80
|
+
list.sort((a, b) => {
|
|
81
|
+
const emailA = String(a?.email || '').toLocaleLowerCase();
|
|
82
|
+
const emailB = String(b?.email || '').toLocaleLowerCase();
|
|
83
|
+
return emailA.localeCompare(emailB);
|
|
84
|
+
});
|
|
85
|
+
setAllUsers(list);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
setAgentErrorKey(err.code || 'Auth.USER_LIST_FAILED');
|
|
88
|
+
} finally {
|
|
89
|
+
setAgentLoading(false);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
51
93
|
useEffect(() => {
|
|
52
94
|
loadRequests();
|
|
53
95
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
54
96
|
}, [statusFilter]);
|
|
55
97
|
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
loadUsers();
|
|
100
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
101
|
+
}, [canManageAgents]);
|
|
102
|
+
|
|
56
103
|
const openDialog = (req) => {
|
|
57
104
|
setSelectedRequest(req);
|
|
58
105
|
setDialogNote('');
|
|
@@ -89,6 +136,57 @@ export function SupportRecoveryRequestsTab() {
|
|
|
89
136
|
}
|
|
90
137
|
};
|
|
91
138
|
|
|
139
|
+
const getUserDisplayName = (entry) => {
|
|
140
|
+
if (entry?.first_name || entry?.last_name) {
|
|
141
|
+
return `${entry.first_name || ''} ${entry.last_name || ''}`.trim();
|
|
142
|
+
}
|
|
143
|
+
return entry?.username || '';
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const getUserOptionLabel = (entry) => {
|
|
147
|
+
if (!entry) return '';
|
|
148
|
+
const name = getUserDisplayName(entry);
|
|
149
|
+
if (name && entry?.email) return `${name} (${entry.email})`;
|
|
150
|
+
return entry?.email || name || `#${entry?.id || ''}`;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const supportAgents = allUsers.filter((entry) => Boolean(entry?.is_support_agent));
|
|
154
|
+
const availableAgentCandidates = allUsers.filter((entry) => !entry?.is_support_agent);
|
|
155
|
+
|
|
156
|
+
const handleAddAgent = async () => {
|
|
157
|
+
if (!selectedAgentCandidate) return;
|
|
158
|
+
|
|
159
|
+
setAgentActionUserId(selectedAgentCandidate.id);
|
|
160
|
+
setAgentErrorKey(null);
|
|
161
|
+
try {
|
|
162
|
+
await updateUserSupportStatus(selectedAgentCandidate.id, true);
|
|
163
|
+
setSelectedAgentCandidate(null);
|
|
164
|
+
await loadUsers();
|
|
165
|
+
} catch (err) {
|
|
166
|
+
setAgentErrorKey(err.code || 'Auth.USER_SUPPORT_UPDATE_FAILED');
|
|
167
|
+
} finally {
|
|
168
|
+
setAgentActionUserId(null);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const handleRemoveAgent = async (agentUser) => {
|
|
173
|
+
if (!agentUser?.id) return;
|
|
174
|
+
|
|
175
|
+
setAgentActionUserId(agentUser.id);
|
|
176
|
+
setAgentErrorKey(null);
|
|
177
|
+
try {
|
|
178
|
+
await updateUserSupportStatus(agentUser.id, false);
|
|
179
|
+
if (selectedAgentCandidate?.id === agentUser.id) {
|
|
180
|
+
setSelectedAgentCandidate(null);
|
|
181
|
+
}
|
|
182
|
+
await loadUsers();
|
|
183
|
+
} catch (err) {
|
|
184
|
+
setAgentErrorKey(err.code || 'Auth.USER_SUPPORT_UPDATE_FAILED');
|
|
185
|
+
} finally {
|
|
186
|
+
setAgentActionUserId(null);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
92
190
|
if (loading) {
|
|
93
191
|
return (
|
|
94
192
|
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
|
|
@@ -99,6 +197,102 @@ export function SupportRecoveryRequestsTab() {
|
|
|
99
197
|
|
|
100
198
|
return (
|
|
101
199
|
<Box>
|
|
200
|
+
{canManageAgents && (
|
|
201
|
+
<Paper variant="outlined" sx={{ p: 2, mb: 3, borderRadius: 2 }}>
|
|
202
|
+
<Typography variant="h6" gutterBottom>
|
|
203
|
+
{t('Support.AGENTS_TITLE', 'Support agents')}
|
|
204
|
+
</Typography>
|
|
205
|
+
|
|
206
|
+
<Typography variant="body2" sx={{ mb: 2 }}>
|
|
207
|
+
{t(
|
|
208
|
+
'Support.AGENTS_DESCRIPTION',
|
|
209
|
+
'Assign which users are allowed to handle support requests.',
|
|
210
|
+
)}
|
|
211
|
+
</Typography>
|
|
212
|
+
|
|
213
|
+
{agentErrorKey && (
|
|
214
|
+
<Alert severity="error" sx={{ mb: 2 }}>
|
|
215
|
+
{t(agentErrorKey)}
|
|
216
|
+
</Alert>
|
|
217
|
+
)}
|
|
218
|
+
|
|
219
|
+
<Stack
|
|
220
|
+
direction={{ xs: 'column', md: 'row' }}
|
|
221
|
+
spacing={2}
|
|
222
|
+
sx={{ mb: 2, alignItems: { md: 'flex-start' } }}
|
|
223
|
+
>
|
|
224
|
+
<Autocomplete
|
|
225
|
+
fullWidth
|
|
226
|
+
options={availableAgentCandidates}
|
|
227
|
+
value={selectedAgentCandidate}
|
|
228
|
+
loading={agentLoading}
|
|
229
|
+
onChange={(_event, value) => setSelectedAgentCandidate(value)}
|
|
230
|
+
isOptionEqualToValue={(option, value) => option.id === value.id}
|
|
231
|
+
getOptionLabel={getUserOptionLabel}
|
|
232
|
+
renderInput={(params) => (
|
|
233
|
+
<TextField
|
|
234
|
+
{...params}
|
|
235
|
+
label={t('Support.AGENTS_SELECT_LABEL', 'Select user')}
|
|
236
|
+
placeholder={t('Support.AGENTS_SELECT_PLACEHOLDER', 'Choose a user')}
|
|
237
|
+
/>
|
|
238
|
+
)}
|
|
239
|
+
/>
|
|
240
|
+
|
|
241
|
+
<Button
|
|
242
|
+
variant="contained"
|
|
243
|
+
onClick={handleAddAgent}
|
|
244
|
+
disabled={!selectedAgentCandidate || agentLoading || Boolean(agentActionUserId)}
|
|
245
|
+
sx={{ minWidth: 160 }}
|
|
246
|
+
>
|
|
247
|
+
{t('Support.AGENTS_ADD', 'Add as agent')}
|
|
248
|
+
</Button>
|
|
249
|
+
</Stack>
|
|
250
|
+
|
|
251
|
+
{agentLoading ? (
|
|
252
|
+
<Box sx={{ display: 'flex', justifyContent: 'center', py: 3 }}>
|
|
253
|
+
<CircularProgress size={24} />
|
|
254
|
+
</Box>
|
|
255
|
+
) : supportAgents.length === 0 ? (
|
|
256
|
+
<Typography variant="body2">
|
|
257
|
+
{t('Support.AGENTS_EMPTY', 'No support agents assigned yet.')}
|
|
258
|
+
</Typography>
|
|
259
|
+
) : (
|
|
260
|
+
<TableContainer component={Paper} variant="outlined">
|
|
261
|
+
<Table size="small">
|
|
262
|
+
<TableHead>
|
|
263
|
+
<TableRow>
|
|
264
|
+
<TableCell>{t('Profile.NAME_LABEL', 'Name')}</TableCell>
|
|
265
|
+
<TableCell>{t('Auth.EMAIL_LABEL', 'Email')}</TableCell>
|
|
266
|
+
<TableCell align="right">
|
|
267
|
+
{t('Common.ACTIONS', 'Actions')}
|
|
268
|
+
</TableCell>
|
|
269
|
+
</TableRow>
|
|
270
|
+
</TableHead>
|
|
271
|
+
<TableBody>
|
|
272
|
+
{supportAgents.map((agentUser) => (
|
|
273
|
+
<TableRow key={agentUser.id}>
|
|
274
|
+
<TableCell>{getUserDisplayName(agentUser) || '-'}</TableCell>
|
|
275
|
+
<TableCell>{agentUser.email || '-'}</TableCell>
|
|
276
|
+
<TableCell align="right">
|
|
277
|
+
<IconButton
|
|
278
|
+
size="small"
|
|
279
|
+
color="error"
|
|
280
|
+
onClick={() => handleRemoveAgent(agentUser)}
|
|
281
|
+
disabled={agentActionUserId === agentUser.id}
|
|
282
|
+
aria-label={t('Support.AGENTS_REMOVE', 'Remove support agent')}
|
|
283
|
+
>
|
|
284
|
+
<CloseIcon fontSize="small" />
|
|
285
|
+
</IconButton>
|
|
286
|
+
</TableCell>
|
|
287
|
+
</TableRow>
|
|
288
|
+
))}
|
|
289
|
+
</TableBody>
|
|
290
|
+
</Table>
|
|
291
|
+
</TableContainer>
|
|
292
|
+
)}
|
|
293
|
+
</Paper>
|
|
294
|
+
)}
|
|
295
|
+
|
|
102
296
|
<Typography variant="h6" gutterBottom>
|
|
103
297
|
{t('Support.RECOVERY_REQUESTS_TITLE', 'Account recovery requests')}
|
|
104
298
|
</Typography>
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { DataGrid } from '@mui/x-data-grid';
|
|
15
15
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|
16
16
|
import { useTranslation } from 'react-i18next';
|
|
17
|
-
import { fetchUsersList, deleteUser, updateUserRole
|
|
17
|
+
import { fetchUsersList, deleteUser, updateUserRole } from '../auth/authApi';
|
|
18
18
|
|
|
19
19
|
const DEFAULT_ROLES = ['none', 'student', 'teacher', 'admin'];
|
|
20
20
|
|
|
@@ -79,15 +79,6 @@ export function UserListComponent({
|
|
|
79
79
|
}
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
-
const handleToggleSupporter = async (userId, newValue) => {
|
|
83
|
-
try {
|
|
84
|
-
await updateUserSupportStatus(userId, newValue);
|
|
85
|
-
await loadUsers();
|
|
86
|
-
} catch (err) {
|
|
87
|
-
alert(t(err.code || 'Auth.USER_UPDATE_FAILED'));
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
|
|
91
82
|
const defaultCanEdit = (targetUser) => {
|
|
92
83
|
if (!currentUser) return false;
|
|
93
84
|
if (currentUser.is_superuser) return true;
|
|
@@ -179,6 +170,7 @@ export function UserListComponent({
|
|
|
179
170
|
};
|
|
180
171
|
|
|
181
172
|
const getSearchBucketForUser = (user) => {
|
|
173
|
+
const hasSuccessfulLogin = Boolean(user?.successful_login ?? user?.last_login);
|
|
182
174
|
const baseValues = [
|
|
183
175
|
user?.id,
|
|
184
176
|
user?.email,
|
|
@@ -188,7 +180,7 @@ export function UserListComponent({
|
|
|
188
180
|
getUserDisplayName(user),
|
|
189
181
|
user?.role,
|
|
190
182
|
user?.language,
|
|
191
|
-
|
|
183
|
+
hasSuccessfulLogin ? t('Common.YES', 'Yes') : t('Common.NO', 'No'),
|
|
192
184
|
];
|
|
193
185
|
|
|
194
186
|
const extraValues = visibleExtraColumns.map((column) => getExtraColumnSearchValue(column, user));
|
|
@@ -241,6 +233,20 @@ export function UserListComponent({
|
|
|
241
233
|
flex: 1,
|
|
242
234
|
valueGetter: (_value, row) => getUserDisplayName(row),
|
|
243
235
|
},
|
|
236
|
+
{
|
|
237
|
+
field: 'successful_login',
|
|
238
|
+
headerName: t('UserList.SUCCESSFUL_LOGIN', 'Successful Login'),
|
|
239
|
+
minWidth: 180,
|
|
240
|
+
flex: 0.8,
|
|
241
|
+
valueGetter: (_value, row) => Boolean(row?.successful_login ?? row?.last_login),
|
|
242
|
+
renderCell: (params) => (
|
|
243
|
+
<Typography variant="body2">
|
|
244
|
+
{Boolean(params.row?.successful_login ?? params.row?.last_login)
|
|
245
|
+
? t('Common.YES', 'Yes')
|
|
246
|
+
: t('Common.NO', 'No')}
|
|
247
|
+
</Typography>
|
|
248
|
+
),
|
|
249
|
+
},
|
|
244
250
|
{
|
|
245
251
|
field: 'role',
|
|
246
252
|
headerName: t('UserList.ROLE', 'Role'),
|
|
@@ -262,28 +268,6 @@ export function UserListComponent({
|
|
|
262
268
|
);
|
|
263
269
|
},
|
|
264
270
|
},
|
|
265
|
-
{
|
|
266
|
-
field: 'is_support_agent',
|
|
267
|
-
headerName: t('UserList.SUPPORTER', 'Support Agent'),
|
|
268
|
-
minWidth: 190,
|
|
269
|
-
flex: 0.8,
|
|
270
|
-
valueGetter: (_value, row) => Boolean(row?.is_support_agent),
|
|
271
|
-
renderCell: (params) => {
|
|
272
|
-
const user = params.row;
|
|
273
|
-
return (
|
|
274
|
-
<FormControl size="small" fullWidth sx={controlSx} disabled={!canEdit(user)}>
|
|
275
|
-
<Select
|
|
276
|
-
value={user.is_support_agent ? 'yes' : 'no'}
|
|
277
|
-
onChange={(event) => handleToggleSupporter(user.id, event.target.value === 'yes')}
|
|
278
|
-
variant="outlined"
|
|
279
|
-
>
|
|
280
|
-
<MenuItem value="yes">{t('Common.YES', 'Yes')}</MenuItem>
|
|
281
|
-
<MenuItem value="no">{t('Common.NO', 'No')}</MenuItem>
|
|
282
|
-
</Select>
|
|
283
|
-
</FormControl>
|
|
284
|
-
);
|
|
285
|
-
},
|
|
286
|
-
},
|
|
287
271
|
];
|
|
288
272
|
|
|
289
273
|
const mappedExtraColumns = visibleExtraColumns.map((column) => ({
|
|
@@ -62,6 +62,13 @@ export function AccountPage({
|
|
|
62
62
|
|
|
63
63
|
// NEU: Superuser-Flag prüfen
|
|
64
64
|
const isSuperUser = user?.is_superuser || false;
|
|
65
|
+
const canViewUsers = Boolean(isSuperUser || perms.can_view_users);
|
|
66
|
+
const canViewInvite = Boolean(isSuperUser || perms.can_view_invite || perms.can_invite);
|
|
67
|
+
const canViewAuthPolicy = Boolean(isSuperUser || perms.can_view_auth_policy);
|
|
68
|
+
const canWriteAuthPolicy = Boolean(isSuperUser || perms.can_write_auth_policy);
|
|
69
|
+
const canSendInvites = Boolean(isSuperUser || perms.can_send_invites);
|
|
70
|
+
const canManageAccessCodes = Boolean(isSuperUser || perms.can_manage_access_codes);
|
|
71
|
+
const canManageSignupQr = Boolean(isSuperUser || perms.can_manage_signup_qr);
|
|
65
72
|
|
|
66
73
|
const handleTabChange = (_event, newValue) => {
|
|
67
74
|
setSearchParams({ tab: newValue });
|
|
@@ -74,7 +81,7 @@ export function AccountPage({
|
|
|
74
81
|
|
|
75
82
|
useEffect(() => {
|
|
76
83
|
let active = true;
|
|
77
|
-
const canLoadPolicy = Boolean(user) && (
|
|
84
|
+
const canLoadPolicy = Boolean(user) && (canViewAuthPolicy || canManageSignupQr || canManageAccessCodes);
|
|
78
85
|
|
|
79
86
|
if (!canLoadPolicy) {
|
|
80
87
|
setAuthPolicy(null);
|
|
@@ -102,7 +109,7 @@ export function AccountPage({
|
|
|
102
109
|
return () => {
|
|
103
110
|
active = false;
|
|
104
111
|
};
|
|
105
|
-
}, [user,
|
|
112
|
+
}, [user, canViewAuthPolicy, canManageSignupQr, canManageAccessCodes, t]);
|
|
106
113
|
|
|
107
114
|
// 3. Dynamic Tabs (angepasst für Superuser)
|
|
108
115
|
const tabs = useMemo(() => {
|
|
@@ -116,11 +123,11 @@ export function AccountPage({
|
|
|
116
123
|
|
|
117
124
|
// Logik: Superuser darf ALLES, sonst gelten die spezifischen Permissions
|
|
118
125
|
|
|
119
|
-
if (
|
|
126
|
+
if (canViewUsers) {
|
|
120
127
|
list.push({ value: 'users', label: t('Account.TAB_USERS', 'Users') });
|
|
121
128
|
}
|
|
122
129
|
|
|
123
|
-
if (
|
|
130
|
+
if (canViewInvite) {
|
|
124
131
|
list.push({ value: 'invite', label: t('Account.TAB_INVITE', 'Invite') });
|
|
125
132
|
}
|
|
126
133
|
|
|
@@ -140,7 +147,7 @@ export function AccountPage({
|
|
|
140
147
|
});
|
|
141
148
|
|
|
142
149
|
return list;
|
|
143
|
-
}, [user, perms, t, isSuperUser, extraTabs]);
|
|
150
|
+
}, [user, perms, t, isSuperUser, extraTabs, canViewUsers, canViewInvite]);
|
|
144
151
|
|
|
145
152
|
// 4. Loading & Auth Checks
|
|
146
153
|
if (loading) {
|
|
@@ -227,29 +234,30 @@ export function AccountPage({
|
|
|
227
234
|
{safeTab === 'invite' && (
|
|
228
235
|
<Box sx={{ mt: 2 }}>
|
|
229
236
|
<Stack spacing={2.5}>
|
|
230
|
-
{
|
|
237
|
+
{canViewAuthPolicy && (
|
|
231
238
|
<Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
|
|
232
|
-
<AuthFactorRequirementCard />
|
|
239
|
+
<AuthFactorRequirementCard canEdit={canWriteAuthPolicy} policy={authPolicy} />
|
|
233
240
|
</Paper>
|
|
234
241
|
)}
|
|
235
242
|
|
|
236
|
-
{
|
|
243
|
+
{canViewAuthPolicy && (
|
|
237
244
|
<Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
|
|
238
245
|
<RegistrationMethodsManager
|
|
239
246
|
policy={authPolicy}
|
|
240
247
|
error={authPolicyError}
|
|
241
248
|
onPolicyChange={setAuthPolicy}
|
|
249
|
+
canEdit={canWriteAuthPolicy}
|
|
242
250
|
/>
|
|
243
251
|
</Paper>
|
|
244
252
|
)}
|
|
245
253
|
|
|
246
|
-
{
|
|
254
|
+
{canSendInvites && Boolean(authPolicy?.allow_admin_invite) && (
|
|
247
255
|
<Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
|
|
248
256
|
<UserInviteComponent />
|
|
249
257
|
</Paper>
|
|
250
258
|
)}
|
|
251
259
|
|
|
252
|
-
{
|
|
260
|
+
{canManageAccessCodes && Boolean(authPolicy?.allow_self_signup_access_code) && (
|
|
253
261
|
<Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
|
|
254
262
|
<Typography variant="h6" gutterBottom>
|
|
255
263
|
{t('Auth.ACCESS_CODE_MANAGER_TITLE', 'Access Codes')}
|
|
@@ -261,33 +269,35 @@ export function AccountPage({
|
|
|
261
269
|
</Paper>
|
|
262
270
|
)}
|
|
263
271
|
|
|
264
|
-
{
|
|
272
|
+
{canSendInvites && showBulkInviteCsvTab && Boolean(authPolicy?.allow_admin_invite) && (
|
|
265
273
|
<Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
|
|
266
274
|
<BulkInviteCsvTab {...bulkInviteCsvProps} />
|
|
267
275
|
</Paper>
|
|
268
276
|
)}
|
|
269
277
|
|
|
270
|
-
{
|
|
278
|
+
{canViewAuthPolicy && Boolean(authPolicy?.allow_self_signup_email_domain) && (
|
|
271
279
|
<Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
|
|
272
280
|
<AllowedEmailDomainsManager
|
|
273
281
|
enabled={Boolean(authPolicy?.allow_self_signup_email_domain)}
|
|
274
282
|
domains={authPolicy?.allowed_email_domains || []}
|
|
275
283
|
onPolicyChange={setAuthPolicy}
|
|
284
|
+
canEdit={canWriteAuthPolicy}
|
|
276
285
|
/>
|
|
277
286
|
</Paper>
|
|
278
287
|
)}
|
|
279
288
|
|
|
280
|
-
{
|
|
289
|
+
{canViewAuthPolicy && Boolean(authPolicy?.allow_self_signup_qr) && (
|
|
281
290
|
<Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
|
|
282
291
|
<QrSignupValidityManager
|
|
283
292
|
enabled={Boolean(authPolicy?.allow_self_signup_qr)}
|
|
284
293
|
expiryDays={authPolicy?.signup_qr_expiry_days}
|
|
285
294
|
onPolicyChange={setAuthPolicy}
|
|
295
|
+
canEdit={canWriteAuthPolicy}
|
|
286
296
|
/>
|
|
287
297
|
</Paper>
|
|
288
298
|
)}
|
|
289
299
|
|
|
290
|
-
{
|
|
300
|
+
{canManageSignupQr && Boolean(authPolicy?.allow_self_signup_qr) && (
|
|
291
301
|
<Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
|
|
292
302
|
<QrSignupManager
|
|
293
303
|
enabled={Boolean(authPolicy?.allow_self_signup_qr)}
|