@micha.bigler/ui-core-micha 2.2.4 → 2.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/QrSignupManager.js +6 -32
- package/dist/components/QrSignupValidityManager.js +48 -0
- package/dist/components/RegistrationMethodsManager.js +22 -32
- package/dist/i18n/authTranslations.js +46 -4
- package/dist/pages/AccountPage.js +33 -3
- package/package.json +1 -1
- package/src/components/QrSignupManager.jsx +5 -55
- package/src/components/QrSignupValidityManager.jsx +100 -0
- package/src/components/RegistrationMethodsManager.jsx +37 -33
- package/src/i18n/authTranslations.ts +46 -4
- package/src/pages/AccountPage.jsx +54 -4
|
@@ -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,
|
|
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
|
|
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,
|
|
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 :
|
|
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', '
|
|
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
|
|
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 {
|
|
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 [
|
|
18
|
+
const [policyState, setPolicyState] = useState(EMPTY_POLICY);
|
|
17
19
|
const [busyField, setBusyField] = useState('');
|
|
18
|
-
const [
|
|
20
|
+
const [saveError, setSaveError] = useState('');
|
|
19
21
|
const [success, setSuccess] = useState('');
|
|
20
22
|
useEffect(() => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 =
|
|
43
|
-
|
|
29
|
+
const previous = policyState[field];
|
|
30
|
+
setPolicyState((prev) => (Object.assign(Object.assign({}, prev), { [field]: checked })));
|
|
44
31
|
setBusyField(field);
|
|
45
|
-
|
|
32
|
+
setSaveError('');
|
|
46
33
|
setSuccess('');
|
|
47
34
|
try {
|
|
48
35
|
const next = await updateAuthPolicy({ [field]: checked });
|
|
49
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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.",
|
|
@@ -1345,10 +1369,22 @@ export const authTranslations = {
|
|
|
1345
1369
|
"sw": "Usajili wa QR"
|
|
1346
1370
|
},
|
|
1347
1371
|
"Auth.SIGNUP_QR_MANAGER_HINT": {
|
|
1348
|
-
"de": "
|
|
1349
|
-
"fr": "
|
|
1350
|
-
"en": "
|
|
1351
|
-
"sw": "
|
|
1372
|
+
"de": "Erzeugen und teilen Sie hier QR-Registrierungslinks.",
|
|
1373
|
+
"fr": "Générez et partagez ici des liens d'inscription QR.",
|
|
1374
|
+
"en": "Generate and share QR signup links below.",
|
|
1375
|
+
"sw": "Tengeneza na ushiriki viungo vya usajili wa QR hapa chini."
|
|
1376
|
+
},
|
|
1377
|
+
"Auth.SIGNUP_QR_VALIDITY_TITLE": {
|
|
1378
|
+
"de": "QR-Registrierung Gültigkeit",
|
|
1379
|
+
"fr": "Validité de l'inscription QR",
|
|
1380
|
+
"en": "QR Signup Validity",
|
|
1381
|
+
"sw": "Uhalali wa usajili wa QR"
|
|
1382
|
+
},
|
|
1383
|
+
"Auth.SIGNUP_QR_VALIDITY_HINT": {
|
|
1384
|
+
"de": "Legen Sie die Standard-Gültigkeit für neu erzeugte QR-Registrierungslinks fest.",
|
|
1385
|
+
"fr": "Définissez la validité par défaut des nouveaux liens d'inscription QR générés.",
|
|
1386
|
+
"en": "Set the default validity for newly generated QR signup links.",
|
|
1387
|
+
"sw": "Weka muda wa kawaida wa uhalali wa viungo vipya vya usajili wa QR."
|
|
1352
1388
|
},
|
|
1353
1389
|
"Auth.SIGNUP_QR_EXPIRY_DAYS_LABEL": {
|
|
1354
1390
|
"de": "QR-Registrierung gültig (Tage)",
|
|
@@ -1440,6 +1476,12 @@ export const authTranslations = {
|
|
|
1440
1476
|
"en": "Sign-Up Access",
|
|
1441
1477
|
"sw": "Ufikiaji wa kujisajili"
|
|
1442
1478
|
},
|
|
1479
|
+
"Auth.SAVE_BUTTON_LOADING": {
|
|
1480
|
+
"de": "Speichern…",
|
|
1481
|
+
"fr": "Enregistrement…",
|
|
1482
|
+
"en": "Saving…",
|
|
1483
|
+
"sw": "Inahifadhi..."
|
|
1484
|
+
},
|
|
1443
1485
|
"Common.YES": {
|
|
1444
1486
|
"de": "Ja",
|
|
1445
1487
|
"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(
|
|
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(RegistrationMethodsManager, { policy: authPolicy, error: authPolicyError, 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: "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
|
@@ -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
|
|
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 ??
|
|
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', '
|
|
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:
|
|
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
|
|
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 {
|
|
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({
|
|
25
|
+
export function RegistrationMethodsManager({
|
|
26
|
+
policy: authPolicy,
|
|
27
|
+
error = '',
|
|
28
|
+
onPolicyChange,
|
|
29
|
+
}) {
|
|
23
30
|
const { t } = useTranslation();
|
|
24
|
-
const [
|
|
31
|
+
const [policyState, setPolicyState] = useState(EMPTY_POLICY);
|
|
25
32
|
const [busyField, setBusyField] = useState('');
|
|
26
|
-
const [
|
|
33
|
+
const [saveError, setSaveError] = useState('');
|
|
27
34
|
const [success, setSuccess] = useState('');
|
|
28
35
|
|
|
29
36
|
useEffect(() => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 =
|
|
50
|
-
|
|
44
|
+
const previous = policyState[field];
|
|
45
|
+
setPolicyState((prev) => ({ ...prev, [field]: checked }));
|
|
51
46
|
setBusyField(field);
|
|
52
|
-
|
|
47
|
+
setSaveError('');
|
|
53
48
|
setSuccess('');
|
|
54
49
|
try {
|
|
55
50
|
const next = await updateAuthPolicy({ [field]: checked });
|
|
56
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
{
|
|
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.",
|
|
@@ -1392,10 +1416,22 @@ export const authTranslations = {
|
|
|
1392
1416
|
"sw": "Usajili wa QR"
|
|
1393
1417
|
},
|
|
1394
1418
|
"Auth.SIGNUP_QR_MANAGER_HINT": {
|
|
1395
|
-
"de": "
|
|
1396
|
-
"fr": "
|
|
1397
|
-
"en": "
|
|
1398
|
-
"sw": "
|
|
1419
|
+
"de": "Erzeugen und teilen Sie hier QR-Registrierungslinks.",
|
|
1420
|
+
"fr": "Générez et partagez ici des liens d'inscription QR.",
|
|
1421
|
+
"en": "Generate and share QR signup links below.",
|
|
1422
|
+
"sw": "Tengeneza na ushiriki viungo vya usajili wa QR hapa chini."
|
|
1423
|
+
},
|
|
1424
|
+
"Auth.SIGNUP_QR_VALIDITY_TITLE": {
|
|
1425
|
+
"de": "QR-Registrierung Gültigkeit",
|
|
1426
|
+
"fr": "Validité de l'inscription QR",
|
|
1427
|
+
"en": "QR Signup Validity",
|
|
1428
|
+
"sw": "Uhalali wa usajili wa QR"
|
|
1429
|
+
},
|
|
1430
|
+
"Auth.SIGNUP_QR_VALIDITY_HINT": {
|
|
1431
|
+
"de": "Legen Sie die Standard-Gültigkeit für neu erzeugte QR-Registrierungslinks fest.",
|
|
1432
|
+
"fr": "Définissez la validité par défaut des nouveaux liens d'inscription QR générés.",
|
|
1433
|
+
"en": "Set the default validity for newly generated QR signup links.",
|
|
1434
|
+
"sw": "Weka muda wa kawaida wa uhalali wa viungo vipya vya usajili wa QR."
|
|
1399
1435
|
},
|
|
1400
1436
|
"Auth.SIGNUP_QR_EXPIRY_DAYS_LABEL": {
|
|
1401
1437
|
"de": "QR-Registrierung gültig (Tage)",
|
|
@@ -1487,6 +1523,12 @@ export const authTranslations = {
|
|
|
1487
1523
|
"en": "Sign-Up Access",
|
|
1488
1524
|
"sw": "Ufikiaji wa kujisajili"
|
|
1489
1525
|
},
|
|
1526
|
+
"Auth.SAVE_BUTTON_LOADING": {
|
|
1527
|
+
"de": "Speichern…",
|
|
1528
|
+
"fr": "Enregistrement…",
|
|
1529
|
+
"en": "Saving…",
|
|
1530
|
+
"sw": "Inahifadhi..."
|
|
1531
|
+
},
|
|
1490
1532
|
"Common.YES": {
|
|
1491
1533
|
"de": "Ja",
|
|
1492
1534
|
"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,7 +229,11 @@ 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
|
|
232
|
+
<RegistrationMethodsManager
|
|
233
|
+
policy={authPolicy}
|
|
234
|
+
error={authPolicyError}
|
|
235
|
+
onPolicyChange={setAuthPolicy}
|
|
236
|
+
/>
|
|
199
237
|
</Paper>
|
|
200
238
|
)}
|
|
201
239
|
|
|
@@ -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
|
-
<
|
|
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
|
)}
|