@micha.bigler/ui-core-micha 2.2.4 → 2.2.6

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.
@@ -45,5 +45,5 @@ export function AuthFactorRequirementCard() {
45
45
  setBusy(false);
46
46
  }
47
47
  };
48
- return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.AUTH_FACTOR_TITLE', 'Authentication Factors') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Auth.AUTH_FACTOR_HINT', 'Choose whether one factor is enough or two factors are required.') }), 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', 'One factor is enough') }), _jsx(FormControlLabel, { value: "2", control: _jsx(Radio, { disabled: busy }), label: t('Auth.TWO_FACTOR_LABEL', 'Two factors are required') })] }) })] }));
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') })] }) })] }));
49
49
  }
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useEffect, useMemo, useRef, useState } from 'react';
3
- import { Alert, Box, Button, Stack, TextField, Typography, } from '@mui/material';
3
+ import { Alert, Box, Button, Stack, Typography, } from '@mui/material';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { QRCodeSVG } from 'qrcode.react';
6
- import { createSignupQr, updateAuthPolicy } from '../auth/authApi';
6
+ import { createSignupQr } from '../auth/authApi';
7
7
  const DEFAULT_EXPIRY_DAYS = 90;
8
8
  function clampExpiryDays(value) {
9
9
  const parsed = parseInt(value, 10);
@@ -20,20 +20,15 @@ function escapeHtml(value) {
20
20
  .replace(/"/g, '"')
21
21
  .replace(/'/g, ''');
22
22
  }
23
- export function QrSignupManager({ enabled = false, expiryDays = DEFAULT_EXPIRY_DAYS, onPolicyChange, }) {
23
+ export function QrSignupManager({ enabled = false, expiryDays = DEFAULT_EXPIRY_DAYS, }) {
24
24
  const { t } = useTranslation();
25
25
  const qrWrapperRef = useRef(null);
26
- const [currentExpiryDays, setCurrentExpiryDays] = useState(String(expiryDays || DEFAULT_EXPIRY_DAYS));
27
26
  const [busy, setBusy] = useState(false);
28
- const [savingPolicy, setSavingPolicy] = useState(false);
29
27
  const [error, setError] = useState('');
30
28
  const [success, setSuccess] = useState('');
31
29
  const [result, setResult] = useState(null);
32
30
  const [copyState, setCopyState] = useState('idle');
33
31
  const hasGeneratedRef = useRef(false);
34
- useEffect(() => {
35
- setCurrentExpiryDays(String(expiryDays || DEFAULT_EXPIRY_DAYS));
36
- }, [expiryDays]);
37
32
  const formattedExpiry = useMemo(() => {
38
33
  if (!(result === null || result === void 0 ? void 0 : result.expires_at)) {
39
34
  return '';
@@ -55,7 +50,7 @@ export function QrSignupManager({ enabled = false, expiryDays = DEFAULT_EXPIRY_D
55
50
  setResult(null);
56
51
  return;
57
52
  }
58
- const nextDays = clampExpiryDays(daysOverride !== null && daysOverride !== void 0 ? daysOverride : currentExpiryDays);
53
+ const nextDays = clampExpiryDays(daysOverride !== null && daysOverride !== void 0 ? daysOverride : expiryDays);
59
54
  setBusy(true);
60
55
  setError('');
61
56
  setSuccess('');
@@ -120,27 +115,6 @@ export function QrSignupManager({ enabled = false, expiryDays = DEFAULT_EXPIRY_D
120
115
  active = false;
121
116
  };
122
117
  }, [enabled, expiryDays, t]);
123
- const handleSavePolicy = async () => {
124
- const nextDays = clampExpiryDays(currentExpiryDays);
125
- setSavingPolicy(true);
126
- setError('');
127
- setSuccess('');
128
- try {
129
- const next = await updateAuthPolicy({
130
- signup_qr_expiry_days: nextDays,
131
- });
132
- setCurrentExpiryDays(String((next === null || next === void 0 ? void 0 : next.signup_qr_expiry_days) || nextDays));
133
- setSuccess(t('Auth.AUTH_POLICY_SAVE_SUCCESS', 'Authentication settings saved.'));
134
- if (onPolicyChange)
135
- onPolicyChange(next);
136
- }
137
- catch (err) {
138
- setError(t((err === null || err === void 0 ? void 0 : err.code) || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save authentication settings.'));
139
- }
140
- finally {
141
- setSavingPolicy(false);
142
- }
143
- };
144
118
  const handleCopyLink = async () => {
145
119
  var _a;
146
120
  const signupUrl = result === null || result === void 0 ? void 0 : result.signup_url;
@@ -270,7 +244,7 @@ export function QrSignupManager({ enabled = false, expiryDays = DEFAULT_EXPIRY_D
270
244
  if (!enabled) {
271
245
  return null;
272
246
  }
273
- return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.SIGNUP_QR_MANAGER_TITLE', 'QR Signup') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Auth.SIGNUP_QR_MANAGER_HINT', 'Save the default validity, then generate and share QR signup links below.') }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error }), success && _jsx(Alert, { severity: "success", sx: { mb: 2 }, children: success }), copyState === 'copied' && (_jsx(Alert, { severity: "success", sx: { mb: 2 }, children: t('Auth.SIGNUP_QR_LINK_COPIED', 'Signup link copied.') })), copyState === 'error' && (_jsx(Alert, { severity: "warning", sx: { mb: 2 }, children: t('Auth.SIGNUP_QR_COPY_UNAVAILABLE', 'Copying the link is not available in this browser.') })), _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: savingPolicy || busy, sx: { flex: 1 } }), _jsx(Button, { variant: "contained", onClick: handleSavePolicy, disabled: savingPolicy || busy, sx: { minWidth: 120, mt: { sm: '8px' } }, children: t('Common.SAVE', 'Save') })] }), (result === null || result === void 0 ? void 0 : result.signup_url) && (_jsxs(Box, { sx: { mt: 3 }, children: [_jsx(Box, { sx: {
247
+ return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.SIGNUP_QR_MANAGER_TITLE', 'QR Signup') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Auth.SIGNUP_QR_MANAGER_HINT', 'Generate and share QR signup links below.') }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error }), success && _jsx(Alert, { severity: "success", sx: { mb: 2 }, children: success }), copyState === 'copied' && (_jsx(Alert, { severity: "success", sx: { mb: 2 }, children: t('Auth.SIGNUP_QR_LINK_COPIED', 'Signup link copied.') })), copyState === 'error' && (_jsx(Alert, { severity: "warning", sx: { mb: 2 }, children: t('Auth.SIGNUP_QR_COPY_UNAVAILABLE', 'Copying the link is not available in this browser.') })), (result === null || result === void 0 ? void 0 : result.signup_url) && (_jsxs(Box, { sx: { mt: 1 }, children: [_jsx(Box, { sx: {
274
248
  display: 'flex',
275
249
  justifyContent: 'center',
276
250
  alignItems: 'center',
@@ -287,5 +261,5 @@ export function QrSignupManager({ enabled = false, expiryDays = DEFAULT_EXPIRY_D
287
261
  bgcolor: 'grey.50',
288
262
  border: '1px solid',
289
263
  borderColor: 'divider',
290
- }, children: [_jsx(Typography, { variant: "subtitle2", gutterBottom: true, children: t('Auth.SIGNUP_QR_ACCESS_TITLE', 'Signup Access') }), _jsxs(Typography, { variant: "body2", color: "text.secondary", children: [t('Auth.SIGNUP_QR_VALID_UNTIL', 'Valid until'), ": ", formattedExpiry || result.expires_at] })] }), _jsxs(Stack, { direction: { xs: 'column', sm: 'row' }, spacing: 1, sx: { mt: 2 }, children: [_jsx(Button, { variant: "outlined", onClick: () => generate(), disabled: busy || savingPolicy, children: t('Auth.SIGNUP_QR_NEW_BUTTON', 'New QR-Code') }), _jsx(Button, { variant: "outlined", onClick: handleCopyLink, disabled: !(result === null || result === void 0 ? void 0 : result.signup_url) || busy, children: t('Auth.SIGNUP_QR_COPY_BUTTON', 'Copy Link') }), _jsx(Button, { variant: "outlined", onClick: handleSavePdf, disabled: !(result === null || result === void 0 ? void 0 : result.signup_url) || busy, children: t('Auth.SIGNUP_QR_PDF_BUTTON', 'Save as PDF') })] })] }))] }));
264
+ }, children: [_jsx(Typography, { variant: "subtitle2", gutterBottom: true, children: t('Auth.SIGNUP_QR_ACCESS_TITLE', 'Signup Access') }), _jsxs(Typography, { variant: "body2", color: "text.secondary", children: [t('Auth.SIGNUP_QR_VALID_UNTIL', 'Valid until'), ": ", formattedExpiry || result.expires_at] })] }), _jsxs(Stack, { direction: { xs: 'column', sm: 'row' }, spacing: 1, sx: { mt: 2 }, children: [_jsx(Button, { variant: "outlined", onClick: () => generate(), disabled: busy, children: t('Auth.SIGNUP_QR_NEW_BUTTON', 'New QR-Code') }), _jsx(Button, { variant: "outlined", onClick: handleCopyLink, disabled: !(result === null || result === void 0 ? void 0 : result.signup_url) || busy, children: t('Auth.SIGNUP_QR_COPY_BUTTON', 'Copy Link') }), _jsx(Button, { variant: "outlined", onClick: handleSavePdf, disabled: !(result === null || result === void 0 ? void 0 : result.signup_url) || busy, children: t('Auth.SIGNUP_QR_PDF_BUTTON', 'Save as PDF') })] })] }))] }));
291
265
  }
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useEffect, useState } from 'react';
3
+ import { Alert, Box, Button, Stack, TextField, Typography, } from '@mui/material';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { updateAuthPolicy } from '../auth/authApi';
6
+ const DEFAULT_EXPIRY_DAYS = 90;
7
+ function clampExpiryDays(value) {
8
+ const parsed = parseInt(value, 10);
9
+ if (!Number.isFinite(parsed) || parsed < 1) {
10
+ return DEFAULT_EXPIRY_DAYS;
11
+ }
12
+ return parsed;
13
+ }
14
+ export function QrSignupValidityManager({ enabled = false, expiryDays = DEFAULT_EXPIRY_DAYS, onPolicyChange, }) {
15
+ const { t } = useTranslation();
16
+ const [currentExpiryDays, setCurrentExpiryDays] = useState(String(expiryDays || DEFAULT_EXPIRY_DAYS));
17
+ const [busy, setBusy] = useState(false);
18
+ const [error, setError] = useState('');
19
+ const [success, setSuccess] = useState('');
20
+ useEffect(() => {
21
+ setCurrentExpiryDays(String(expiryDays || DEFAULT_EXPIRY_DAYS));
22
+ }, [expiryDays]);
23
+ const handleSave = async () => {
24
+ const nextDays = clampExpiryDays(currentExpiryDays);
25
+ setBusy(true);
26
+ setError('');
27
+ setSuccess('');
28
+ try {
29
+ const next = await updateAuthPolicy({
30
+ signup_qr_expiry_days: nextDays,
31
+ });
32
+ setCurrentExpiryDays(String((next === null || next === void 0 ? void 0 : next.signup_qr_expiry_days) || nextDays));
33
+ setSuccess(t('Auth.AUTH_POLICY_SAVE_SUCCESS', 'Authentication settings saved.'));
34
+ if (onPolicyChange)
35
+ onPolicyChange(next);
36
+ }
37
+ catch (err) {
38
+ setError(t((err === null || err === void 0 ? void 0 : err.code) || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save authentication settings.'));
39
+ }
40
+ finally {
41
+ setBusy(false);
42
+ }
43
+ };
44
+ if (!enabled) {
45
+ return null;
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') })] })] }));
48
+ }
@@ -1,63 +1,53 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useEffect, useState } from 'react';
3
- import { Alert, Box, FormControlLabel, Stack, Switch, Typography, } from '@mui/material';
3
+ import { Alert, Box, CircularProgress, FormControlLabel, Stack, Switch, Typography, } from '@mui/material';
4
4
  import { useTranslation } from 'react-i18next';
5
- import { fetchAuthPolicy, updateAuthPolicy } from '../auth/authApi';
5
+ import { updateAuthPolicy } from '../auth/authApi';
6
6
  const EMPTY_POLICY = {
7
7
  allow_admin_invite: true,
8
8
  allow_self_signup_access_code: false,
9
9
  allow_self_signup_open: false,
10
10
  allow_self_signup_email_domain: false,
11
11
  allow_self_signup_qr: false,
12
+ allowed_email_domains: [],
13
+ signup_qr_expiry_days: 90,
12
14
  required_auth_factor_count: 1,
13
15
  };
14
- export function RegistrationMethodsManager({ onPolicyChange }) {
16
+ export function RegistrationMethodsManager({ policy: authPolicy, error = '', onPolicyChange, }) {
15
17
  const { t } = useTranslation();
16
- const [policy, setPolicy] = useState(EMPTY_POLICY);
18
+ const [policyState, setPolicyState] = useState(EMPTY_POLICY);
17
19
  const [busyField, setBusyField] = useState('');
18
- const [error, setError] = useState('');
20
+ const [saveError, setSaveError] = useState('');
19
21
  const [success, setSuccess] = useState('');
20
22
  useEffect(() => {
21
- let active = true;
22
- (async () => {
23
- try {
24
- const data = await fetchAuthPolicy();
25
- if (!active)
26
- return;
27
- setPolicy((prev) => (Object.assign(Object.assign({}, prev), data)));
28
- if (onPolicyChange)
29
- onPolicyChange(data);
30
- }
31
- catch (err) {
32
- if (active) {
33
- setError(t((err === null || err === void 0 ? void 0 : err.code) || 'Auth.AUTH_POLICY_FETCH_FAILED', 'Could not load authentication policy.'));
34
- }
35
- }
36
- })();
37
- return () => {
38
- active = false;
39
- };
40
- }, [onPolicyChange, t]);
23
+ if (!authPolicy) {
24
+ return;
25
+ }
26
+ setPolicyState(Object.assign(Object.assign({}, EMPTY_POLICY), authPolicy));
27
+ }, [authPolicy]);
41
28
  const toggle = (field) => async (_event, checked) => {
42
- const previous = policy[field];
43
- setPolicy((prev) => (Object.assign(Object.assign({}, prev), { [field]: checked })));
29
+ const previous = policyState[field];
30
+ setPolicyState((prev) => (Object.assign(Object.assign({}, prev), { [field]: checked })));
44
31
  setBusyField(field);
45
- setError('');
32
+ setSaveError('');
46
33
  setSuccess('');
47
34
  try {
48
35
  const next = await updateAuthPolicy({ [field]: checked });
49
- setPolicy((prev) => (Object.assign(Object.assign({}, prev), next)));
36
+ setPolicyState((prev) => (Object.assign(Object.assign({}, prev), next)));
50
37
  setSuccess(t('Auth.AUTH_POLICY_SAVE_SUCCESS', 'Authentication settings saved.'));
51
38
  if (onPolicyChange)
52
39
  onPolicyChange(next);
53
40
  }
54
41
  catch (err) {
55
- setPolicy((prev) => (Object.assign(Object.assign({}, prev), { [field]: previous })));
56
- setError(t((err === null || err === void 0 ? void 0 : err.code) || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save authentication settings.'));
42
+ setPolicyState((prev) => (Object.assign(Object.assign({}, prev), { [field]: previous })));
43
+ setSaveError(t((err === null || err === void 0 ? void 0 : err.code) || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save authentication settings.'));
57
44
  }
58
45
  finally {
59
46
  setBusyField('');
60
47
  }
61
48
  };
62
- 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 }), success && _jsx(Alert, { severity: "success", sx: { mb: 2 }, children: success }), _jsxs(Stack, { spacing: 1, children: [_jsx(FormControlLabel, { control: (_jsx(Switch, { checked: Boolean(policy.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(policy.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(policy.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(policy.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(policy.allow_self_signup_qr), onChange: toggle('allow_self_signup_qr'), disabled: Boolean(busyField) })), label: t('Auth.SIGNUP_QR_LABEL', 'Self-signup by QR') })] }), policy.allow_self_signup_email_domain && !(policy.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.') }))] }));
49
+ if (!authPolicy && !error) {
50
+ return (_jsx(Box, { sx: { py: 3, display: 'flex', justifyContent: 'center' }, children: _jsx(CircularProgress, { size: 28 }) }));
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.') }))] }));
63
53
  }
@@ -456,6 +456,12 @@ export const authTranslations = {
456
456
  "en": "Add code manually",
457
457
  "sw": "Ongeza msimbo mwenyewe"
458
458
  },
459
+ "Auth.ACCESS_CODE_MANAGER_TITLE": {
460
+ "de": "Zugangscodes",
461
+ "fr": "Codes d'accès",
462
+ "en": "Access Codes",
463
+ "sw": "Misimbo ya ufikiaji"
464
+ },
459
465
  "Auth.ACCESS_CODE_LABEL": {
460
466
  "de": "Zugangscode",
461
467
  "fr": "Code d'accès",
@@ -468,6 +474,24 @@ export const authTranslations = {
468
474
  "en": "Add",
469
475
  "sw": "Ongeza"
470
476
  },
477
+ "Auth.ACCESS_CODE_COPY_BUTTON": {
478
+ "de": "Kopieren",
479
+ "fr": "Copier",
480
+ "en": "Copy",
481
+ "sw": "Nakili"
482
+ },
483
+ "Auth.ACCESS_CODE_COPY_SUCCESS": {
484
+ "de": "Code kopiert.",
485
+ "fr": "Code copié.",
486
+ "en": "Code copied.",
487
+ "sw": "Msimbo umenakiliwa."
488
+ },
489
+ "Auth.ACCESS_CODE_COPY_FALLBACK": {
490
+ "de": "Code markieren und mit Strg+C kopieren.",
491
+ "fr": "Sélectionnez le code et copiez-le avec Ctrl+C.",
492
+ "en": "Select the code and copy it with Ctrl+C.",
493
+ "sw": "Chagua msimbo na unakili kwa Ctrl+C."
494
+ },
471
495
  "Auth.ACCESS_CODE_LIST_FAILED": {
472
496
  "de": "Zugangscodes konnten nicht geladen werden.",
473
497
  "fr": "Impossible de charger les codes d'accès.",
@@ -1266,6 +1290,36 @@ export const authTranslations = {
1266
1290
  "en": "Authentication settings saved.",
1267
1291
  "sw": "Mipangilio ya uthibitishaji imehifadhiwa."
1268
1292
  },
1293
+ "Auth.AUTH_FACTOR_TITLE": {
1294
+ "de": "Authentifizierungsanforderungen",
1295
+ "fr": "Exigences d'authentification",
1296
+ "en": "Authentication Requirements",
1297
+ "sw": "Mahitaji ya uthibitishaji"
1298
+ },
1299
+ "Auth.AUTH_FACTOR_HINT": {
1300
+ "de": "Legen Sie fest, wie viele Authentifizierungsfaktoren mindestens für die Anmeldung erforderlich sind.",
1301
+ "fr": "Définissez le nombre minimal de facteurs d'authentification requis pour la connexion.",
1302
+ "en": "Define the minimum number of authentication factors required for sign-in.",
1303
+ "sw": "Weka idadi ya chini ya vipengele vya uthibitishaji vinavyohitajika ili kuingia."
1304
+ },
1305
+ "Auth.ONE_FACTOR_LABEL": {
1306
+ "de": "Single-Factor-Authentifizierung erlauben",
1307
+ "fr": "Autoriser l'authentification à un facteur",
1308
+ "en": "Allow single-factor authentication",
1309
+ "sw": "Ruhusu uthibitishaji wa kipengele kimoja"
1310
+ },
1311
+ "Auth.TWO_FACTOR_LABEL": {
1312
+ "de": "Zwei-Faktor-Authentifizierung erzwingen",
1313
+ "fr": "Exiger l'authentification à deux facteurs",
1314
+ "en": "Require two-factor authentication",
1315
+ "sw": "Hitaji uthibitishaji wa vipengele viwili"
1316
+ },
1317
+ "Auth.AUTH_FACTOR_SAVE_SUCCESS": {
1318
+ "de": "Die Anforderung für Authentifizierungsfaktoren wurde gespeichert.",
1319
+ "fr": "L'exigence relative aux facteurs d'authentification a été enregistrée.",
1320
+ "en": "Factor requirement saved.",
1321
+ "sw": "Hitaji la vipengele vya uthibitishaji limehifadhiwa."
1322
+ },
1269
1323
  "Auth.REGISTRATION_METHODS_TITLE": {
1270
1324
  "de": "Registrierungsmethoden",
1271
1325
  "fr": "Méthodes d'inscription",
@@ -1345,10 +1399,22 @@ export const authTranslations = {
1345
1399
  "sw": "Usajili wa QR"
1346
1400
  },
1347
1401
  "Auth.SIGNUP_QR_MANAGER_HINT": {
1348
- "de": "Speichern Sie zuerst die Standard-Gültigkeit und erzeugen Sie darunter neue QR-Registrierungslinks.",
1349
- "fr": "Enregistrez d'abord la validité par défaut, puis générez ci-dessous de nouveaux liens d'inscription QR.",
1350
- "en": "Save the default validity, then generate and share QR signup links below.",
1351
- "sw": "Hifadhi muda wa kawaida wa uhalali kwanza, kisha tengeneza na ushiriki viungo vya usajili wa QR hapa chini."
1402
+ "de": "Erzeugen und teilen Sie hier QR-Registrierungslinks.",
1403
+ "fr": "Générez et partagez ici des liens d'inscription QR.",
1404
+ "en": "Generate and share QR signup links below.",
1405
+ "sw": "Tengeneza na ushiriki viungo vya usajili wa QR hapa chini."
1406
+ },
1407
+ "Auth.SIGNUP_QR_VALIDITY_TITLE": {
1408
+ "de": "QR-Registrierung Gültigkeit",
1409
+ "fr": "Validité de l'inscription QR",
1410
+ "en": "QR Signup Validity",
1411
+ "sw": "Uhalali wa usajili wa QR"
1412
+ },
1413
+ "Auth.SIGNUP_QR_VALIDITY_HINT": {
1414
+ "de": "Legen Sie die Standard-Gültigkeit für neu erzeugte QR-Registrierungslinks fest.",
1415
+ "fr": "Définissez la validité par défaut des nouveaux liens d'inscription QR générés.",
1416
+ "en": "Set the default validity for newly generated QR signup links.",
1417
+ "sw": "Weka muda wa kawaida wa uhalali wa viungo vipya vya usajili wa QR."
1352
1418
  },
1353
1419
  "Auth.SIGNUP_QR_EXPIRY_DAYS_LABEL": {
1354
1420
  "de": "QR-Registrierung gültig (Tage)",
@@ -1440,6 +1506,12 @@ export const authTranslations = {
1440
1506
  "en": "Sign-Up Access",
1441
1507
  "sw": "Ufikiaji wa kujisajili"
1442
1508
  },
1509
+ "Auth.SAVE_BUTTON_LOADING": {
1510
+ "de": "Speichern…",
1511
+ "fr": "Enregistrement…",
1512
+ "en": "Saving…",
1513
+ "sw": "Inahifadhi..."
1514
+ },
1443
1515
  "Common.YES": {
1444
1516
  "de": "Ja",
1445
1517
  "fr": "Oui",
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React, { useContext, useMemo, useState } from 'react';
2
+ import React, { useContext, useEffect, useMemo, useState } from 'react';
3
3
  import { Helmet } from 'react-helmet';
4
4
  import { useSearchParams } from 'react-router-dom';
5
5
  import { Tabs, Tab, Box, Typography, Alert, CircularProgress, Paper, Stack, } from '@mui/material';
@@ -17,15 +17,17 @@ import { AllowedEmailDomainsManager } from '../components/AllowedEmailDomainsMan
17
17
  import { RegistrationMethodsManager } from '../components/RegistrationMethodsManager';
18
18
  import { AuthFactorRequirementCard } from '../components/AuthFactorRequirementCard';
19
19
  import { QrSignupManager } from '../components/QrSignupManager';
20
+ import { QrSignupValidityManager } from '../components/QrSignupValidityManager';
20
21
  import { SupportRecoveryRequestsTab } from '../components/SupportRecoveryRequestsTab';
21
22
  import { BulkInviteCsvTab } from '../components/BulkInviteCsvTab';
22
- import { updateUserProfile } from '../auth/authApi'; // Ggf. Pfad anpassen
23
+ import { fetchAuthPolicy, updateUserProfile } from '../auth/authApi'; // Ggf. Pfad anpassen
23
24
  export function AccountPage({ userListExtraColumns = [], userListExtraRowActions = [], userListExtraContext = null, userListRefreshTrigger = 0, userListCanEditUser = null, showBulkInviteCsvTab = false, bulkInviteCsvProps = {}, extraTabs = [], }) {
24
25
  var _a;
25
26
  const { t } = useTranslation();
26
27
  const { user, login, loading } = useContext(AuthContext);
27
28
  const [searchParams, setSearchParams] = useSearchParams();
28
29
  const [authPolicy, setAuthPolicy] = useState(null);
30
+ const [authPolicyError, setAuthPolicyError] = useState('');
29
31
  // 1. URL State Management
30
32
  const currentTabRaw = searchParams.get('tab') || 'profile';
31
33
  const currentTab = ['invite', 'bulk-invite-csv', 'access'].includes(currentTabRaw)
@@ -45,6 +47,34 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
45
47
  const updatedUser = await updateUserProfile(payload);
46
48
  login(updatedUser);
47
49
  };
50
+ useEffect(() => {
51
+ let active = true;
52
+ const canLoadPolicy = Boolean(user) && (isSuperUser || perms.can_invite || perms.can_manage_access_codes);
53
+ if (!canLoadPolicy) {
54
+ setAuthPolicy(null);
55
+ setAuthPolicyError('');
56
+ return undefined;
57
+ }
58
+ const loadPolicy = async () => {
59
+ try {
60
+ const data = await fetchAuthPolicy();
61
+ if (!active)
62
+ return;
63
+ setAuthPolicy(data);
64
+ setAuthPolicyError('');
65
+ }
66
+ catch (err) {
67
+ if (!active)
68
+ return;
69
+ setAuthPolicy(null);
70
+ setAuthPolicyError(t((err === null || err === void 0 ? void 0 : err.code) || 'Auth.AUTH_POLICY_FETCH_FAILED', 'Could not load authentication policy.'));
71
+ }
72
+ };
73
+ loadPolicy();
74
+ return () => {
75
+ active = false;
76
+ };
77
+ }, [user, isSuperUser, perms.can_invite, perms.can_manage_access_codes, t]);
48
78
  // 3. Dynamic Tabs (angepasst für Superuser)
49
79
  const tabs = useMemo(() => {
50
80
  if (!user)
@@ -92,5 +122,5 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
92
122
  const activeExtraTab = builtInTabValues.has(safeTab)
93
123
  ? null
94
124
  : extraTabs.find((tab) => tab.value === safeTab);
95
- 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(RegistrationMethodsManager, { onPolicyChange: setAuthPolicy }) })), (isSuperUser || perms.can_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(AuthFactorRequirementCard, {}) })), (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: "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(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, onPolicyChange: setAuthPolicy }) }))] }) })), 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 }) }))] }));
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 }) }))] }));
96
126
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "2.2.4",
3
+ "version": "2.2.6",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "private": false,
@@ -20,6 +20,7 @@
20
20
  "build": "tsc -p tsconfig.build.json"
21
21
  },
22
22
  "devDependencies": {
23
+ "typescript": "^5.9.3"
23
24
  },
24
25
  "dependencies": {
25
26
  "react-i18next": "^16.3.5"
@@ -56,17 +56,28 @@ export function AuthFactorRequirementCard() {
56
56
  return (
57
57
  <Box>
58
58
  <Typography variant="h6" gutterBottom>
59
- {t('Auth.AUTH_FACTOR_TITLE', 'Authentication Factors')}
59
+ {t('Auth.AUTH_FACTOR_TITLE', 'Authentication Requirements')}
60
60
  </Typography>
61
61
  <Typography variant="body2" sx={{ mb: 2, color: 'text.secondary' }}>
62
- {t('Auth.AUTH_FACTOR_HINT', 'Choose whether one factor is enough or two factors are required.')}
62
+ {t(
63
+ 'Auth.AUTH_FACTOR_HINT',
64
+ 'Define the minimum number of authentication factors required for sign-in.',
65
+ )}
63
66
  </Typography>
64
67
  {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
65
68
  {success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
66
69
  <FormControl>
67
70
  <RadioGroup value={value} onChange={handleChange}>
68
- <FormControlLabel value="1" control={<Radio disabled={busy} />} label={t('Auth.ONE_FACTOR_LABEL', 'One factor is enough')} />
69
- <FormControlLabel value="2" control={<Radio disabled={busy} />} label={t('Auth.TWO_FACTOR_LABEL', 'Two factors are required')} />
71
+ <FormControlLabel
72
+ value="1"
73
+ control={<Radio disabled={busy} />}
74
+ label={t('Auth.ONE_FACTOR_LABEL', 'Allow single-factor authentication')}
75
+ />
76
+ <FormControlLabel
77
+ value="2"
78
+ control={<Radio disabled={busy} />}
79
+ label={t('Auth.TWO_FACTOR_LABEL', 'Require two-factor authentication')}
80
+ />
70
81
  </RadioGroup>
71
82
  </FormControl>
72
83
  </Box>
@@ -4,12 +4,11 @@ import {
4
4
  Box,
5
5
  Button,
6
6
  Stack,
7
- TextField,
8
7
  Typography,
9
8
  } from '@mui/material';
10
9
  import { useTranslation } from 'react-i18next';
11
10
  import { QRCodeSVG } from 'qrcode.react';
12
- import { createSignupQr, updateAuthPolicy } from '../auth/authApi';
11
+ import { createSignupQr } from '../auth/authApi';
13
12
 
14
13
  const DEFAULT_EXPIRY_DAYS = 90;
15
14
 
@@ -33,23 +32,16 @@ function escapeHtml(value) {
33
32
  export function QrSignupManager({
34
33
  enabled = false,
35
34
  expiryDays = DEFAULT_EXPIRY_DAYS,
36
- onPolicyChange,
37
35
  }) {
38
36
  const { t } = useTranslation();
39
37
  const qrWrapperRef = useRef(null);
40
- const [currentExpiryDays, setCurrentExpiryDays] = useState(String(expiryDays || DEFAULT_EXPIRY_DAYS));
41
38
  const [busy, setBusy] = useState(false);
42
- const [savingPolicy, setSavingPolicy] = useState(false);
43
39
  const [error, setError] = useState('');
44
40
  const [success, setSuccess] = useState('');
45
41
  const [result, setResult] = useState(null);
46
42
  const [copyState, setCopyState] = useState('idle');
47
43
  const hasGeneratedRef = useRef(false);
48
44
 
49
- useEffect(() => {
50
- setCurrentExpiryDays(String(expiryDays || DEFAULT_EXPIRY_DAYS));
51
- }, [expiryDays]);
52
-
53
45
  const formattedExpiry = useMemo(() => {
54
46
  if (!result?.expires_at) {
55
47
  return '';
@@ -72,7 +64,7 @@ export function QrSignupManager({
72
64
  setResult(null);
73
65
  return;
74
66
  }
75
- const nextDays = clampExpiryDays(daysOverride ?? currentExpiryDays);
67
+ const nextDays = clampExpiryDays(daysOverride ?? expiryDays);
76
68
  setBusy(true);
77
69
  setError('');
78
70
  setSuccess('');
@@ -133,25 +125,6 @@ export function QrSignupManager({
133
125
  };
134
126
  }, [enabled, expiryDays, t]);
135
127
 
136
- const handleSavePolicy = async () => {
137
- const nextDays = clampExpiryDays(currentExpiryDays);
138
- setSavingPolicy(true);
139
- setError('');
140
- setSuccess('');
141
- try {
142
- const next = await updateAuthPolicy({
143
- signup_qr_expiry_days: nextDays,
144
- });
145
- setCurrentExpiryDays(String(next?.signup_qr_expiry_days || nextDays));
146
- setSuccess(t('Auth.AUTH_POLICY_SAVE_SUCCESS', 'Authentication settings saved.'));
147
- if (onPolicyChange) onPolicyChange(next);
148
- } catch (err) {
149
- setError(t(err?.code || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save authentication settings.'));
150
- } finally {
151
- setSavingPolicy(false);
152
- }
153
- };
154
-
155
128
  const handleCopyLink = async () => {
156
129
  const signupUrl = result?.signup_url;
157
130
  if (!signupUrl || !navigator?.clipboard?.writeText) {
@@ -290,7 +263,7 @@ export function QrSignupManager({
290
263
  {t('Auth.SIGNUP_QR_MANAGER_TITLE', 'QR Signup')}
291
264
  </Typography>
292
265
  <Typography variant="body2" sx={{ mb: 2, color: 'text.secondary' }}>
293
- {t('Auth.SIGNUP_QR_MANAGER_HINT', 'Save the default validity, then generate and share QR signup links below.')}
266
+ {t('Auth.SIGNUP_QR_MANAGER_HINT', 'Generate and share QR signup links below.')}
294
267
  </Typography>
295
268
 
296
269
  {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
@@ -306,31 +279,8 @@ export function QrSignupManager({
306
279
  </Alert>
307
280
  )}
308
281
 
309
- <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} alignItems={{ sm: 'flex-start' }}>
310
- <TextField
311
- label={t('Auth.SIGNUP_QR_EXPIRY_DAYS_LABEL', 'QR signup validity (days)')}
312
- helperText={t(
313
- 'Auth.SIGNUP_QR_EXPIRY_DAYS_HINT',
314
- 'Default validity for newly generated QR signup links.',
315
- )}
316
- type="number"
317
- value={currentExpiryDays}
318
- onChange={(event) => setCurrentExpiryDays(event.target.value)}
319
- disabled={savingPolicy || busy}
320
- sx={{ flex: 1 }}
321
- />
322
- <Button
323
- variant="contained"
324
- onClick={handleSavePolicy}
325
- disabled={savingPolicy || busy}
326
- sx={{ minWidth: 120, mt: { sm: '8px' } }}
327
- >
328
- {t('Common.SAVE', 'Save')}
329
- </Button>
330
- </Stack>
331
-
332
282
  {result?.signup_url && (
333
- <Box sx={{ mt: 3 }}>
283
+ <Box sx={{ mt: 1 }}>
334
284
  <Box
335
285
  sx={{
336
286
  display: 'flex',
@@ -368,7 +318,7 @@ export function QrSignupManager({
368
318
  </Box>
369
319
 
370
320
  <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1} sx={{ mt: 2 }}>
371
- <Button variant="outlined" onClick={() => generate()} disabled={busy || savingPolicy}>
321
+ <Button variant="outlined" onClick={() => generate()} disabled={busy}>
372
322
  {t('Auth.SIGNUP_QR_NEW_BUTTON', 'New QR-Code')}
373
323
  </Button>
374
324
  <Button variant="outlined" onClick={handleCopyLink} disabled={!result?.signup_url || busy}>
@@ -0,0 +1,100 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import {
3
+ Alert,
4
+ Box,
5
+ Button,
6
+ Stack,
7
+ TextField,
8
+ Typography,
9
+ } from '@mui/material';
10
+ import { useTranslation } from 'react-i18next';
11
+ import { updateAuthPolicy } from '../auth/authApi';
12
+
13
+ const DEFAULT_EXPIRY_DAYS = 90;
14
+
15
+ function clampExpiryDays(value) {
16
+ const parsed = parseInt(value, 10);
17
+ if (!Number.isFinite(parsed) || parsed < 1) {
18
+ return DEFAULT_EXPIRY_DAYS;
19
+ }
20
+ return parsed;
21
+ }
22
+
23
+ export function QrSignupValidityManager({
24
+ enabled = false,
25
+ expiryDays = DEFAULT_EXPIRY_DAYS,
26
+ onPolicyChange,
27
+ }) {
28
+ const { t } = useTranslation();
29
+ const [currentExpiryDays, setCurrentExpiryDays] = useState(String(expiryDays || DEFAULT_EXPIRY_DAYS));
30
+ const [busy, setBusy] = useState(false);
31
+ const [error, setError] = useState('');
32
+ const [success, setSuccess] = useState('');
33
+
34
+ useEffect(() => {
35
+ setCurrentExpiryDays(String(expiryDays || DEFAULT_EXPIRY_DAYS));
36
+ }, [expiryDays]);
37
+
38
+ const handleSave = async () => {
39
+ const nextDays = clampExpiryDays(currentExpiryDays);
40
+ setBusy(true);
41
+ setError('');
42
+ setSuccess('');
43
+ try {
44
+ const next = await updateAuthPolicy({
45
+ signup_qr_expiry_days: nextDays,
46
+ });
47
+ setCurrentExpiryDays(String(next?.signup_qr_expiry_days || nextDays));
48
+ setSuccess(t('Auth.AUTH_POLICY_SAVE_SUCCESS', 'Authentication settings saved.'));
49
+ if (onPolicyChange) onPolicyChange(next);
50
+ } catch (err) {
51
+ setError(t(err?.code || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save authentication settings.'));
52
+ } finally {
53
+ setBusy(false);
54
+ }
55
+ };
56
+
57
+ if (!enabled) {
58
+ return null;
59
+ }
60
+
61
+ return (
62
+ <Box>
63
+ <Typography variant="h6" gutterBottom>
64
+ {t('Auth.SIGNUP_QR_VALIDITY_TITLE', 'QR Signup Validity')}
65
+ </Typography>
66
+ <Typography variant="body2" sx={{ mb: 2, color: 'text.secondary' }}>
67
+ {t(
68
+ 'Auth.SIGNUP_QR_VALIDITY_HINT',
69
+ 'Set the default validity for newly generated QR signup links.',
70
+ )}
71
+ </Typography>
72
+
73
+ {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
74
+ {success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
75
+
76
+ <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} alignItems={{ sm: 'flex-start' }}>
77
+ <TextField
78
+ label={t('Auth.SIGNUP_QR_EXPIRY_DAYS_LABEL', 'QR signup validity (days)')}
79
+ helperText={t(
80
+ 'Auth.SIGNUP_QR_EXPIRY_DAYS_HINT',
81
+ 'Default validity for newly generated QR signup links.',
82
+ )}
83
+ type="number"
84
+ value={currentExpiryDays}
85
+ onChange={(event) => setCurrentExpiryDays(event.target.value)}
86
+ disabled={busy}
87
+ sx={{ flex: 1 }}
88
+ />
89
+ <Button
90
+ variant="contained"
91
+ onClick={handleSave}
92
+ disabled={busy}
93
+ sx={{ minWidth: 120, mt: { sm: '8px' } }}
94
+ >
95
+ {t('Common.SAVE', 'Save')}
96
+ </Button>
97
+ </Stack>
98
+ </Box>
99
+ );
100
+ }
@@ -2,13 +2,14 @@ import React, { useEffect, useState } from 'react';
2
2
  import {
3
3
  Alert,
4
4
  Box,
5
+ CircularProgress,
5
6
  FormControlLabel,
6
7
  Stack,
7
8
  Switch,
8
9
  Typography,
9
10
  } from '@mui/material';
10
11
  import { useTranslation } from 'react-i18next';
11
- import { fetchAuthPolicy, updateAuthPolicy } from '../auth/authApi';
12
+ import { updateAuthPolicy } from '../auth/authApi';
12
13
 
13
14
  const EMPTY_POLICY = {
14
15
  allow_admin_invite: true,
@@ -16,54 +17,56 @@ const EMPTY_POLICY = {
16
17
  allow_self_signup_open: false,
17
18
  allow_self_signup_email_domain: false,
18
19
  allow_self_signup_qr: false,
20
+ allowed_email_domains: [],
21
+ signup_qr_expiry_days: 90,
19
22
  required_auth_factor_count: 1,
20
23
  };
21
24
 
22
- export function RegistrationMethodsManager({ onPolicyChange }) {
25
+ export function RegistrationMethodsManager({
26
+ policy: authPolicy,
27
+ error = '',
28
+ onPolicyChange,
29
+ }) {
23
30
  const { t } = useTranslation();
24
- const [policy, setPolicy] = useState(EMPTY_POLICY);
31
+ const [policyState, setPolicyState] = useState(EMPTY_POLICY);
25
32
  const [busyField, setBusyField] = useState('');
26
- const [error, setError] = useState('');
33
+ const [saveError, setSaveError] = useState('');
27
34
  const [success, setSuccess] = useState('');
28
35
 
29
36
  useEffect(() => {
30
- let active = true;
31
- (async () => {
32
- try {
33
- const data = await fetchAuthPolicy();
34
- if (!active) return;
35
- setPolicy((prev) => ({ ...prev, ...data }));
36
- if (onPolicyChange) onPolicyChange(data);
37
- } catch (err) {
38
- if (active) {
39
- setError(t(err?.code || 'Auth.AUTH_POLICY_FETCH_FAILED', 'Could not load authentication policy.'));
40
- }
41
- }
42
- })();
43
- return () => {
44
- active = false;
45
- };
46
- }, [onPolicyChange, t]);
37
+ if (!authPolicy) {
38
+ return;
39
+ }
40
+ setPolicyState({ ...EMPTY_POLICY, ...authPolicy });
41
+ }, [authPolicy]);
47
42
 
48
43
  const toggle = (field) => async (_event, checked) => {
49
- const previous = policy[field];
50
- setPolicy((prev) => ({ ...prev, [field]: checked }));
44
+ const previous = policyState[field];
45
+ setPolicyState((prev) => ({ ...prev, [field]: checked }));
51
46
  setBusyField(field);
52
- setError('');
47
+ setSaveError('');
53
48
  setSuccess('');
54
49
  try {
55
50
  const next = await updateAuthPolicy({ [field]: checked });
56
- setPolicy((prev) => ({ ...prev, ...next }));
51
+ setPolicyState((prev) => ({ ...prev, ...next }));
57
52
  setSuccess(t('Auth.AUTH_POLICY_SAVE_SUCCESS', 'Authentication settings saved.'));
58
53
  if (onPolicyChange) onPolicyChange(next);
59
54
  } catch (err) {
60
- setPolicy((prev) => ({ ...prev, [field]: previous }));
61
- setError(t(err?.code || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save authentication settings.'));
55
+ setPolicyState((prev) => ({ ...prev, [field]: previous }));
56
+ setSaveError(t(err?.code || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save authentication settings.'));
62
57
  } finally {
63
58
  setBusyField('');
64
59
  }
65
60
  };
66
61
 
62
+ if (!authPolicy && !error) {
63
+ return (
64
+ <Box sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
65
+ <CircularProgress size={28} />
66
+ </Box>
67
+ );
68
+ }
69
+
67
70
  return (
68
71
  <Box>
69
72
  <Typography variant="h6" gutterBottom>
@@ -73,13 +76,14 @@ export function RegistrationMethodsManager({ onPolicyChange }) {
73
76
  {t('Auth.REGISTRATION_METHODS_HINT', 'Choose which signup and invite flows are active for this app.')}
74
77
  </Typography>
75
78
  {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
79
+ {saveError && <Alert severity="error" sx={{ mb: 2 }}>{saveError}</Alert>}
76
80
  {success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
77
81
 
78
82
  <Stack spacing={1}>
79
83
  <FormControlLabel
80
84
  control={(
81
85
  <Switch
82
- checked={Boolean(policy.allow_admin_invite)}
86
+ checked={Boolean(policyState.allow_admin_invite)}
83
87
  onChange={toggle('allow_admin_invite')}
84
88
  disabled={Boolean(busyField)}
85
89
  />
@@ -89,7 +93,7 @@ export function RegistrationMethodsManager({ onPolicyChange }) {
89
93
  <FormControlLabel
90
94
  control={(
91
95
  <Switch
92
- checked={Boolean(policy.allow_self_signup_access_code)}
96
+ checked={Boolean(policyState.allow_self_signup_access_code)}
93
97
  onChange={toggle('allow_self_signup_access_code')}
94
98
  disabled={Boolean(busyField)}
95
99
  />
@@ -99,7 +103,7 @@ export function RegistrationMethodsManager({ onPolicyChange }) {
99
103
  <FormControlLabel
100
104
  control={(
101
105
  <Switch
102
- checked={Boolean(policy.allow_self_signup_open)}
106
+ checked={Boolean(policyState.allow_self_signup_open)}
103
107
  onChange={toggle('allow_self_signup_open')}
104
108
  disabled={Boolean(busyField)}
105
109
  />
@@ -109,7 +113,7 @@ export function RegistrationMethodsManager({ onPolicyChange }) {
109
113
  <FormControlLabel
110
114
  control={(
111
115
  <Switch
112
- checked={Boolean(policy.allow_self_signup_email_domain)}
116
+ checked={Boolean(policyState.allow_self_signup_email_domain)}
113
117
  onChange={toggle('allow_self_signup_email_domain')}
114
118
  disabled={Boolean(busyField)}
115
119
  />
@@ -119,7 +123,7 @@ export function RegistrationMethodsManager({ onPolicyChange }) {
119
123
  <FormControlLabel
120
124
  control={(
121
125
  <Switch
122
- checked={Boolean(policy.allow_self_signup_qr)}
126
+ checked={Boolean(policyState.allow_self_signup_qr)}
123
127
  onChange={toggle('allow_self_signup_qr')}
124
128
  disabled={Boolean(busyField)}
125
129
  />
@@ -128,7 +132,7 @@ export function RegistrationMethodsManager({ onPolicyChange }) {
128
132
  />
129
133
  </Stack>
130
134
 
131
- {policy.allow_self_signup_email_domain && !(policy.allowed_email_domains || []).length && (
135
+ {policyState.allow_self_signup_email_domain && !(policyState.allowed_email_domains || []).length && (
132
136
  <Alert severity="info" sx={{ mt: 2 }}>
133
137
  {t(
134
138
  'Auth.EMAIL_DOMAIN_CURRENTLY_BLOCKED_HINT',
@@ -497,6 +497,12 @@ export const authTranslations = {
497
497
  "en": "Add code manually",
498
498
  "sw": "Ongeza msimbo mwenyewe"
499
499
  },
500
+ "Auth.ACCESS_CODE_MANAGER_TITLE": {
501
+ "de": "Zugangscodes",
502
+ "fr": "Codes d'accès",
503
+ "en": "Access Codes",
504
+ "sw": "Misimbo ya ufikiaji"
505
+ },
500
506
  "Auth.ACCESS_CODE_LABEL": {
501
507
  "de": "Zugangscode",
502
508
  "fr": "Code d'accès",
@@ -509,6 +515,24 @@ export const authTranslations = {
509
515
  "en": "Add",
510
516
  "sw": "Ongeza"
511
517
  },
518
+ "Auth.ACCESS_CODE_COPY_BUTTON": {
519
+ "de": "Kopieren",
520
+ "fr": "Copier",
521
+ "en": "Copy",
522
+ "sw": "Nakili"
523
+ },
524
+ "Auth.ACCESS_CODE_COPY_SUCCESS": {
525
+ "de": "Code kopiert.",
526
+ "fr": "Code copié.",
527
+ "en": "Code copied.",
528
+ "sw": "Msimbo umenakiliwa."
529
+ },
530
+ "Auth.ACCESS_CODE_COPY_FALLBACK": {
531
+ "de": "Code markieren und mit Strg+C kopieren.",
532
+ "fr": "Sélectionnez le code et copiez-le avec Ctrl+C.",
533
+ "en": "Select the code and copy it with Ctrl+C.",
534
+ "sw": "Chagua msimbo na unakili kwa Ctrl+C."
535
+ },
512
536
  "Auth.ACCESS_CODE_LIST_FAILED": {
513
537
  "de": "Zugangscodes konnten nicht geladen werden.",
514
538
  "fr": "Impossible de charger les codes d'accès.",
@@ -1313,6 +1337,36 @@ export const authTranslations = {
1313
1337
  "en": "Authentication settings saved.",
1314
1338
  "sw": "Mipangilio ya uthibitishaji imehifadhiwa."
1315
1339
  },
1340
+ "Auth.AUTH_FACTOR_TITLE": {
1341
+ "de": "Authentifizierungsanforderungen",
1342
+ "fr": "Exigences d'authentification",
1343
+ "en": "Authentication Requirements",
1344
+ "sw": "Mahitaji ya uthibitishaji"
1345
+ },
1346
+ "Auth.AUTH_FACTOR_HINT": {
1347
+ "de": "Legen Sie fest, wie viele Authentifizierungsfaktoren mindestens für die Anmeldung erforderlich sind.",
1348
+ "fr": "Définissez le nombre minimal de facteurs d'authentification requis pour la connexion.",
1349
+ "en": "Define the minimum number of authentication factors required for sign-in.",
1350
+ "sw": "Weka idadi ya chini ya vipengele vya uthibitishaji vinavyohitajika ili kuingia."
1351
+ },
1352
+ "Auth.ONE_FACTOR_LABEL": {
1353
+ "de": "Single-Factor-Authentifizierung erlauben",
1354
+ "fr": "Autoriser l'authentification à un facteur",
1355
+ "en": "Allow single-factor authentication",
1356
+ "sw": "Ruhusu uthibitishaji wa kipengele kimoja"
1357
+ },
1358
+ "Auth.TWO_FACTOR_LABEL": {
1359
+ "de": "Zwei-Faktor-Authentifizierung erzwingen",
1360
+ "fr": "Exiger l'authentification à deux facteurs",
1361
+ "en": "Require two-factor authentication",
1362
+ "sw": "Hitaji uthibitishaji wa vipengele viwili"
1363
+ },
1364
+ "Auth.AUTH_FACTOR_SAVE_SUCCESS": {
1365
+ "de": "Die Anforderung für Authentifizierungsfaktoren wurde gespeichert.",
1366
+ "fr": "L'exigence relative aux facteurs d'authentification a été enregistrée.",
1367
+ "en": "Factor requirement saved.",
1368
+ "sw": "Hitaji la vipengele vya uthibitishaji limehifadhiwa."
1369
+ },
1316
1370
  "Auth.REGISTRATION_METHODS_TITLE": {
1317
1371
  "de": "Registrierungsmethoden",
1318
1372
  "fr": "Méthodes d'inscription",
@@ -1392,10 +1446,22 @@ export const authTranslations = {
1392
1446
  "sw": "Usajili wa QR"
1393
1447
  },
1394
1448
  "Auth.SIGNUP_QR_MANAGER_HINT": {
1395
- "de": "Speichern Sie zuerst die Standard-Gültigkeit und erzeugen Sie darunter neue QR-Registrierungslinks.",
1396
- "fr": "Enregistrez d'abord la validité par défaut, puis générez ci-dessous de nouveaux liens d'inscription QR.",
1397
- "en": "Save the default validity, then generate and share QR signup links below.",
1398
- "sw": "Hifadhi muda wa kawaida wa uhalali kwanza, kisha tengeneza na ushiriki viungo vya usajili wa QR hapa chini."
1449
+ "de": "Erzeugen und teilen Sie hier QR-Registrierungslinks.",
1450
+ "fr": "Générez et partagez ici des liens d'inscription QR.",
1451
+ "en": "Generate and share QR signup links below.",
1452
+ "sw": "Tengeneza na ushiriki viungo vya usajili wa QR hapa chini."
1453
+ },
1454
+ "Auth.SIGNUP_QR_VALIDITY_TITLE": {
1455
+ "de": "QR-Registrierung Gültigkeit",
1456
+ "fr": "Validité de l'inscription QR",
1457
+ "en": "QR Signup Validity",
1458
+ "sw": "Uhalali wa usajili wa QR"
1459
+ },
1460
+ "Auth.SIGNUP_QR_VALIDITY_HINT": {
1461
+ "de": "Legen Sie die Standard-Gültigkeit für neu erzeugte QR-Registrierungslinks fest.",
1462
+ "fr": "Définissez la validité par défaut des nouveaux liens d'inscription QR générés.",
1463
+ "en": "Set the default validity for newly generated QR signup links.",
1464
+ "sw": "Weka muda wa kawaida wa uhalali wa viungo vipya vya usajili wa QR."
1399
1465
  },
1400
1466
  "Auth.SIGNUP_QR_EXPIRY_DAYS_LABEL": {
1401
1467
  "de": "QR-Registrierung gültig (Tage)",
@@ -1487,6 +1553,12 @@ export const authTranslations = {
1487
1553
  "en": "Sign-Up Access",
1488
1554
  "sw": "Ufikiaji wa kujisajili"
1489
1555
  },
1556
+ "Auth.SAVE_BUTTON_LOADING": {
1557
+ "de": "Speichern…",
1558
+ "fr": "Enregistrement…",
1559
+ "en": "Saving…",
1560
+ "sw": "Inahifadhi..."
1561
+ },
1490
1562
  "Common.YES": {
1491
1563
  "de": "Ja",
1492
1564
  "fr": "Oui",
@@ -1,4 +1,4 @@
1
- import React, { useContext, useMemo, useState } from 'react';
1
+ import React, { useContext, useEffect, useMemo, useState } from 'react';
2
2
  import { Helmet } from 'react-helmet';
3
3
  import { useSearchParams } from 'react-router-dom';
4
4
  import {
@@ -27,9 +27,10 @@ import { AllowedEmailDomainsManager } from '../components/AllowedEmailDomainsMan
27
27
  import { RegistrationMethodsManager } from '../components/RegistrationMethodsManager';
28
28
  import { AuthFactorRequirementCard } from '../components/AuthFactorRequirementCard';
29
29
  import { QrSignupManager } from '../components/QrSignupManager';
30
+ import { QrSignupValidityManager } from '../components/QrSignupValidityManager';
30
31
  import { SupportRecoveryRequestsTab } from '../components/SupportRecoveryRequestsTab';
31
32
  import { BulkInviteCsvTab } from '../components/BulkInviteCsvTab';
32
- import { updateUserProfile } from '../auth/authApi'; // Ggf. Pfad anpassen
33
+ import { fetchAuthPolicy, updateUserProfile } from '../auth/authApi'; // Ggf. Pfad anpassen
33
34
 
34
35
  export function AccountPage({
35
36
  userListExtraColumns = [],
@@ -45,6 +46,7 @@ export function AccountPage({
45
46
  const { user, login, loading } = useContext(AuthContext);
46
47
  const [searchParams, setSearchParams] = useSearchParams();
47
48
  const [authPolicy, setAuthPolicy] = useState(null);
49
+ const [authPolicyError, setAuthPolicyError] = useState('');
48
50
 
49
51
  // 1. URL State Management
50
52
  const currentTabRaw = searchParams.get('tab') || 'profile';
@@ -70,6 +72,38 @@ export function AccountPage({
70
72
  login(updatedUser);
71
73
  };
72
74
 
75
+ useEffect(() => {
76
+ let active = true;
77
+ const canLoadPolicy = Boolean(user) && (isSuperUser || perms.can_invite || perms.can_manage_access_codes);
78
+
79
+ if (!canLoadPolicy) {
80
+ setAuthPolicy(null);
81
+ setAuthPolicyError('');
82
+ return undefined;
83
+ }
84
+
85
+ const loadPolicy = async () => {
86
+ try {
87
+ const data = await fetchAuthPolicy();
88
+ if (!active) return;
89
+ setAuthPolicy(data);
90
+ setAuthPolicyError('');
91
+ } catch (err) {
92
+ if (!active) return;
93
+ setAuthPolicy(null);
94
+ setAuthPolicyError(
95
+ t(err?.code || 'Auth.AUTH_POLICY_FETCH_FAILED', 'Could not load authentication policy.'),
96
+ );
97
+ }
98
+ };
99
+
100
+ loadPolicy();
101
+
102
+ return () => {
103
+ active = false;
104
+ };
105
+ }, [user, isSuperUser, perms.can_invite, perms.can_manage_access_codes, t]);
106
+
73
107
  // 3. Dynamic Tabs (angepasst für Superuser)
74
108
  const tabs = useMemo(() => {
75
109
  if (!user) return [];
@@ -195,13 +229,17 @@ export function AccountPage({
195
229
  <Stack spacing={2.5}>
196
230
  {(isSuperUser || perms.can_invite) && (
197
231
  <Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
198
- <RegistrationMethodsManager onPolicyChange={setAuthPolicy} />
232
+ <AuthFactorRequirementCard />
199
233
  </Paper>
200
234
  )}
201
235
 
202
236
  {(isSuperUser || perms.can_invite) && (
203
237
  <Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
204
- <AuthFactorRequirementCard />
238
+ <RegistrationMethodsManager
239
+ policy={authPolicy}
240
+ error={authPolicyError}
241
+ onPolicyChange={setAuthPolicy}
242
+ />
205
243
  </Paper>
206
244
  )}
207
245
 
@@ -213,6 +251,9 @@ export function AccountPage({
213
251
 
214
252
  {(isSuperUser || perms.can_manage_access_codes) && Boolean(authPolicy?.allow_self_signup_access_code) && (
215
253
  <Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
254
+ <Typography variant="h6" gutterBottom>
255
+ {t('Auth.ACCESS_CODE_MANAGER_TITLE', 'Access Codes')}
256
+ </Typography>
216
257
  <Typography variant="body2" sx={{ mb: 2, color: 'text.secondary' }}>
217
258
  {t('Account.ACCESS_CODES_HINT', 'Manage access codes for self-registration.')}
218
259
  </Typography>
@@ -238,13 +279,22 @@ export function AccountPage({
238
279
 
239
280
  {(isSuperUser || perms.can_invite) && Boolean(authPolicy?.allow_self_signup_qr) && (
240
281
  <Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
241
- <QrSignupManager
282
+ <QrSignupValidityManager
242
283
  enabled={Boolean(authPolicy?.allow_self_signup_qr)}
243
284
  expiryDays={authPolicy?.signup_qr_expiry_days}
244
285
  onPolicyChange={setAuthPolicy}
245
286
  />
246
287
  </Paper>
247
288
  )}
289
+
290
+ {(isSuperUser || perms.can_invite) && Boolean(authPolicy?.allow_self_signup_qr) && (
291
+ <Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
292
+ <QrSignupManager
293
+ enabled={Boolean(authPolicy?.allow_self_signup_qr)}
294
+ expiryDays={authPolicy?.signup_qr_expiry_days}
295
+ />
296
+ </Paper>
297
+ )}
248
298
  </Stack>
249
299
  </Box>
250
300
  )}