@micha.bigler/ui-core-micha 2.2.6 → 2.2.8
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 +56 -29
- package/dist/pages/AccountPage.js +14 -7
- 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 +78 -53
- package/src/pages/AccountPage.jsx +34 -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,9 +4,9 @@ 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
|
-
export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraColumns = [], extraRowActions = [], extraContext = null, refreshTrigger = 0, canEditUser, }) {
|
|
9
|
+
export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraColumns = [], extraRowActions = [], extraContext = null, refreshTrigger = 0, canEditUser, showRoleColumn = true, onChangeRole = null, showDeleteAction = true, canDeleteUser = null, onDeleteUser = null, }) {
|
|
10
10
|
const { t } = useTranslation();
|
|
11
11
|
const [users, setUsers] = useState([]);
|
|
12
12
|
const [loading, setLoading] = useState(true);
|
|
@@ -44,7 +44,12 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
44
44
|
if (!window.confirm(t('UserList.DELETE_CONFIRM', 'Are you sure you want to delete this user?')))
|
|
45
45
|
return;
|
|
46
46
|
try {
|
|
47
|
-
|
|
47
|
+
if (typeof onDeleteUser === 'function') {
|
|
48
|
+
await onDeleteUser({ userId, currentUser, extraContext, t, reloadUsers: loadUsers });
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
await deleteUser(userId);
|
|
52
|
+
}
|
|
48
53
|
setUsers(prev => prev.filter(u => u.id !== userId));
|
|
49
54
|
}
|
|
50
55
|
catch (err) {
|
|
@@ -53,22 +58,25 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
53
58
|
};
|
|
54
59
|
const handleChangeRole = async (userId, newRole) => {
|
|
55
60
|
try {
|
|
56
|
-
|
|
61
|
+
if (typeof onChangeRole === 'function') {
|
|
62
|
+
await onChangeRole({
|
|
63
|
+
userId,
|
|
64
|
+
newRole,
|
|
65
|
+
currentUser,
|
|
66
|
+
extraContext,
|
|
67
|
+
t,
|
|
68
|
+
reloadUsers: loadUsers,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
await updateUserRole(userId, newRole);
|
|
73
|
+
}
|
|
57
74
|
await loadUsers();
|
|
58
75
|
}
|
|
59
76
|
catch (err) {
|
|
60
77
|
alert(t(err.code || 'Auth.USER_ROLE_UPDATE_FAILED'));
|
|
61
78
|
}
|
|
62
79
|
};
|
|
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
80
|
const defaultCanEdit = (targetUser) => {
|
|
73
81
|
if (!currentUser)
|
|
74
82
|
return false;
|
|
@@ -93,6 +101,12 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
93
101
|
}
|
|
94
102
|
return defaultCanEdit(targetUser);
|
|
95
103
|
};
|
|
104
|
+
const canDelete = (targetUser) => {
|
|
105
|
+
if (typeof canDeleteUser === 'function') {
|
|
106
|
+
return Boolean(canDeleteUser({ targetUser, currentUser, extraContext }));
|
|
107
|
+
}
|
|
108
|
+
return canEdit(targetUser);
|
|
109
|
+
};
|
|
96
110
|
const listContext = useMemo(() => ({
|
|
97
111
|
currentUser,
|
|
98
112
|
extraContext,
|
|
@@ -142,6 +156,8 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
142
156
|
return String(value).toLocaleLowerCase();
|
|
143
157
|
};
|
|
144
158
|
const getSearchBucketForUser = (user) => {
|
|
159
|
+
var _a;
|
|
160
|
+
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
161
|
const baseValues = [
|
|
146
162
|
user === null || user === void 0 ? void 0 : user.id,
|
|
147
163
|
user === null || user === void 0 ? void 0 : user.email,
|
|
@@ -151,7 +167,7 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
151
167
|
getUserDisplayName(user),
|
|
152
168
|
user === null || user === void 0 ? void 0 : user.role,
|
|
153
169
|
user === null || user === void 0 ? void 0 : user.language,
|
|
154
|
-
|
|
170
|
+
hasSuccessfulLogin ? t('Common.YES', 'Yes') : t('Common.NO', 'No'),
|
|
155
171
|
];
|
|
156
172
|
const extraValues = visibleExtraColumns.map((column) => getExtraColumnSearchValue(column, user));
|
|
157
173
|
return [...baseValues, ...extraValues].map((value) => toSearchText(value)).filter(Boolean);
|
|
@@ -202,28 +218,32 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
202
218
|
valueGetter: (_value, row) => getUserDisplayName(row),
|
|
203
219
|
},
|
|
204
220
|
{
|
|
205
|
-
field: '
|
|
206
|
-
headerName: t('UserList.
|
|
221
|
+
field: 'successful_login',
|
|
222
|
+
headerName: t('UserList.SUCCESSFUL_LOGIN', 'Successful Login'),
|
|
207
223
|
minWidth: 180,
|
|
208
224
|
flex: 0.8,
|
|
209
|
-
valueGetter: (_value, row) => (row === null || row === void 0 ? void 0 : row.
|
|
225
|
+
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
226
|
renderCell: (params) => {
|
|
211
|
-
|
|
212
|
-
return (_jsx(
|
|
227
|
+
var _a, _b, _c;
|
|
228
|
+
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)
|
|
229
|
+
? t('Common.YES', 'Yes')
|
|
230
|
+
: t('Common.NO', 'No') }));
|
|
213
231
|
},
|
|
214
232
|
},
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
233
|
+
];
|
|
234
|
+
if (showRoleColumn) {
|
|
235
|
+
baseColumns.push({
|
|
236
|
+
field: 'role',
|
|
237
|
+
headerName: t('UserList.ROLE', 'Role'),
|
|
238
|
+
minWidth: 180,
|
|
219
239
|
flex: 0.8,
|
|
220
|
-
valueGetter: (_value, row) =>
|
|
240
|
+
valueGetter: (_value, row) => (row === null || row === void 0 ? void 0 : row.role) || 'none',
|
|
221
241
|
renderCell: (params) => {
|
|
222
242
|
const user = params.row;
|
|
223
|
-
return (_jsx(FormControl, { size: "small", fullWidth: true, sx: controlSx, disabled: !canEdit(user), children:
|
|
243
|
+
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
244
|
},
|
|
225
|
-
}
|
|
226
|
-
|
|
245
|
+
});
|
|
246
|
+
}
|
|
227
247
|
const mappedExtraColumns = visibleExtraColumns.map((column) => ({
|
|
228
248
|
field: `extra:${column.key}`,
|
|
229
249
|
headerName: typeof column.label === 'function' ? column.label(listContext) : column.label,
|
|
@@ -242,10 +262,14 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
242
262
|
reloadUsers: loadUsers,
|
|
243
263
|
}),
|
|
244
264
|
}));
|
|
265
|
+
const hasActionColumn = showDeleteAction || visibleRowActions.length > 0;
|
|
266
|
+
if (!hasActionColumn) {
|
|
267
|
+
return [...baseColumns, ...mappedExtraColumns];
|
|
268
|
+
}
|
|
245
269
|
const actionColumn = {
|
|
246
270
|
field: 'actions',
|
|
247
271
|
headerName: t('Common.ACTIONS', 'Actions'),
|
|
248
|
-
minWidth: Math.max(220, 110 + visibleRowActions.length * 110),
|
|
272
|
+
minWidth: Math.max(220, 110 + (visibleRowActions.length + (showDeleteAction ? 1 : 0)) * 110),
|
|
249
273
|
flex: 1.4,
|
|
250
274
|
sortable: false,
|
|
251
275
|
filterable: false,
|
|
@@ -267,13 +291,15 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
267
291
|
return (_jsx(Button, { size: "small", variant: "outlined", onClick: () => runRowAction(action, user), disabled: isBusy || isDisabled, sx: actionButtonSx, children: typeof action.label === 'function'
|
|
268
292
|
? action.label({ user, t, currentUser, canEdit: canEdit(user) })
|
|
269
293
|
: action.label }, `${action.key}-${user.id}`));
|
|
270
|
-
}), _jsx(Tooltip, { title: t('Common.DELETE', 'Delete'), children: _jsx("span", { children: _jsx(Button, { size: "small", variant: "outlined", color: "error", startIcon: _jsx(DeleteIcon, {}), onClick: () => handleDelete(user.id), disabled: !
|
|
294
|
+
}), showDeleteAction && (_jsx(Tooltip, { title: t('Common.DELETE', 'Delete'), children: _jsx("span", { children: _jsx(Button, { size: "small", variant: "outlined", color: "error", startIcon: _jsx(DeleteIcon, {}), onClick: () => handleDelete(user.id), disabled: !canDelete(user), sx: actionButtonSx, children: t('Common.DELETE', 'Delete') }) }) }))] }));
|
|
271
295
|
},
|
|
272
296
|
};
|
|
273
297
|
return [...baseColumns, ...mappedExtraColumns, actionColumn];
|
|
274
298
|
}, [
|
|
275
299
|
t,
|
|
276
300
|
roles,
|
|
301
|
+
showRoleColumn,
|
|
302
|
+
showDeleteAction,
|
|
277
303
|
visibleExtraColumns,
|
|
278
304
|
visibleRowActions,
|
|
279
305
|
listContext,
|
|
@@ -282,6 +308,7 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
282
308
|
extraContext,
|
|
283
309
|
loadUsers,
|
|
284
310
|
canEdit,
|
|
311
|
+
canDelete,
|
|
285
312
|
]);
|
|
286
313
|
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('UserList.TITLE', 'All Users') }), _jsx(Box, { sx: { mb: 2, maxWidth: 420 }, children: _jsx(TextField, { fullWidth: true, size: "small", label: t('Common.SEARCH', 'Search'), placeholder: t('UserList.SEARCH_PLACEHOLDER', 'Search users...'), value: searchQuery, onChange: (event) => setSearchQuery(event.target.value) }) }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(error) }), loading && (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', p: 3 }, children: _jsx(CircularProgress, {}) })), !loading && (_jsx(Box, { sx: { width: '100%', minHeight: 520 }, children: _jsx(DataGrid, { rows: filteredUsers, columns: columns, disableRowSelectionOnClick: true, showToolbar: true, getRowHeight: () => 'auto', pageSizeOptions: [10, 25, 50, 100], initialState: {
|
|
287
314
|
sorting: { sortModel: [{ field: 'email', sort: 'asc' }] },
|
|
@@ -21,7 +21,7 @@ import { QrSignupValidityManager } from '../components/QrSignupValidityManager';
|
|
|
21
21
|
import { SupportRecoveryRequestsTab } from '../components/SupportRecoveryRequestsTab';
|
|
22
22
|
import { BulkInviteCsvTab } from '../components/BulkInviteCsvTab';
|
|
23
23
|
import { fetchAuthPolicy, updateUserProfile } from '../auth/authApi'; // Ggf. Pfad anpassen
|
|
24
|
-
export function AccountPage({ userListExtraColumns = [], userListExtraRowActions = [], userListExtraContext = null, userListRefreshTrigger = 0, userListCanEditUser = null, showBulkInviteCsvTab = false, bulkInviteCsvProps = {}, extraTabs = [], }) {
|
|
24
|
+
export function AccountPage({ userListExtraColumns = [], userListExtraRowActions = [], userListExtraContext = null, userListRefreshTrigger = 0, userListCanEditUser = null, userListShowRoleColumn = true, userListOnChangeRole = null, userListShowDeleteAction = true, userListCanDeleteUser = null, userListOnDeleteUser = null, showBulkInviteCsvTab = false, bulkInviteCsvProps = {}, extraTabs = [], }) {
|
|
25
25
|
var _a;
|
|
26
26
|
const { t } = useTranslation();
|
|
27
27
|
const { user, login, loading } = useContext(AuthContext);
|
|
@@ -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, showRoleColumn: userListShowRoleColumn, onChangeRole: userListOnChangeRole, showDeleteAction: userListShowDeleteAction, canDeleteUser: userListCanDeleteUser, onDeleteUser: userListOnDeleteUser }) })), 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>
|