@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.
@@ -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, Typography, Table, TableHead, TableBody, TableRow, TableCell, TableContainer, Paper, Button, CircularProgress, Alert, Stack, Dialog, DialogTitle, DialogContent, DialogActions, TextField, } from '@mui/material';
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 { fetchRecoveryRequests, approveRecoveryRequest, rejectRecoveryRequest } from '../auth/authApi';
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, updateUserSupportStatus } from '../auth/authApi';
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
- await deleteUser(userId);
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
- await updateUserRole(userId, newRole);
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
- (user === null || user === void 0 ? void 0 : user.is_support_agent) ? t('Common.YES', 'Yes') : t('Common.NO', 'No'),
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: 'role',
206
- headerName: t('UserList.ROLE', 'Role'),
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.role) || 'none',
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
- const user = params.row;
212
- 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)) }) }));
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
- field: 'is_support_agent',
217
- headerName: t('UserList.SUPPORTER', 'Support Agent'),
218
- minWidth: 190,
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) => Boolean(row === null || row === void 0 ? void 0 : row.is_support_agent),
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: _jsxs(Select, { value: user.is_support_agent ? 'yes' : 'no', onChange: (event) => handleToggleSupporter(user.id, event.target.value === 'yes'), variant: "outlined", children: [_jsx(MenuItem, { value: "yes", children: t('Common.YES', 'Yes') }), _jsx(MenuItem, { value: "no", children: t('Common.NO', 'No') })] }) }));
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: !canEdit(user), sx: actionButtonSx, children: t('Common.DELETE', 'Delete') }) }) })] }));
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) && (isSuperUser || perms.can_invite || perms.can_manage_access_codes);
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, isSuperUser, perms.can_invite, perms.can_manage_access_codes, t]);
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 (isSuperUser || perms.can_view_users) {
95
+ if (canViewUsers) {
89
96
  list.push({ value: 'users', label: t('Account.TAB_USERS', 'Users') });
90
97
  }
91
- if (isSuperUser || perms.can_invite || perms.can_manage_access_codes) {
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: [(isSuperUser || perms.can_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(AuthFactorRequirementCard, {}) })), (isSuperUser || perms.can_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(RegistrationMethodsManager, { policy: authPolicy, error: authPolicyError, onPolicyChange: setAuthPolicy }) })), (isSuperUser || perms.can_invite) && 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, {}) })), (isSuperUser || perms.can_manage_access_codes) && 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, {})] })), (isSuperUser || perms.can_invite) && 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)) })), (isSuperUser || perms.can_invite) && 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 }) })), (isSuperUser || perms.can_invite) && 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 }) })), (isSuperUser || perms.can_invite) && 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 }) }))] }));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "2.2.6",
3
+ "version": "2.2.8",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "private": false,
@@ -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>