@micha.bigler/ui-core-micha 2.2.3 → 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.
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useEffect, useState } from 'react';
3
+ import { Alert, Box, Button, TextField, Typography, } from '@mui/material';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { updateAuthPolicy } from '../auth/authApi';
6
+ export function AllowedEmailDomainsManager({ domains = [], enabled = false, onPolicyChange, }) {
7
+ const { t } = useTranslation();
8
+ const [domainsText, setDomainsText] = useState('');
9
+ const [busy, setBusy] = useState(false);
10
+ const [error, setError] = useState('');
11
+ const [success, setSuccess] = useState('');
12
+ useEffect(() => {
13
+ setDomainsText((domains || []).join('\n'));
14
+ }, [domains]);
15
+ const handleSave = async () => {
16
+ setBusy(true);
17
+ setError('');
18
+ setSuccess('');
19
+ try {
20
+ const allowedEmailDomains = domainsText
21
+ .split(/\r?\n/)
22
+ .map((value) => value.trim())
23
+ .filter(Boolean);
24
+ const next = await updateAuthPolicy({
25
+ allowed_email_domains: allowedEmailDomains,
26
+ });
27
+ setDomainsText(((next === null || next === void 0 ? void 0 : next.allowed_email_domains) || allowedEmailDomains).join('\n'));
28
+ setSuccess(t('Auth.AUTH_POLICY_SAVE_SUCCESS', 'Authentication settings saved.'));
29
+ if (onPolicyChange)
30
+ onPolicyChange(next);
31
+ }
32
+ catch (err) {
33
+ setError(t((err === null || err === void 0 ? void 0 : err.code) || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save authentication settings.'));
34
+ }
35
+ finally {
36
+ setBusy(false);
37
+ }
38
+ };
39
+ if (!enabled) {
40
+ return null;
41
+ }
42
+ return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.ALLOWED_EMAIL_DOMAINS_TITLE', 'Allowed Email Domains') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Auth.ALLOWED_EMAIL_DOMAINS_CARD_HINT', 'Only addresses from these domains can use email-domain sign-up.') }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error }), success && _jsx(Alert, { severity: "success", sx: { mb: 2 }, children: success }), _jsx(TextField, { label: t('Auth.ALLOWED_EMAIL_DOMAINS_LABEL', 'Allowed email domains'), helperText: t('Auth.ALLOWED_EMAIL_DOMAINS_HINT', 'One domain per line, e.g. example.org. You can leave this empty temporarily.'), multiline: true, minRows: 4, fullWidth: true, value: domainsText, onChange: (event) => setDomainsText(event.target.value), disabled: busy }), _jsx(Button, { variant: "contained", sx: { mt: 2 }, onClick: handleSave, disabled: busy, children: t('Common.SAVE', 'Save') })] }));
43
+ }
@@ -1,32 +1,67 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React, { useEffect, useRef, useState } from 'react';
3
- import { Alert, Box, Button, TextField, Typography, } from '@mui/material';
2
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
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
6
  import { createSignupQr } from '../auth/authApi';
7
- export function QrSignupManager({ enabled = false }) {
7
+ const DEFAULT_EXPIRY_DAYS = 90;
8
+ function clampExpiryDays(value) {
9
+ const parsed = parseInt(value, 10);
10
+ if (!Number.isFinite(parsed) || parsed < 1) {
11
+ return DEFAULT_EXPIRY_DAYS;
12
+ }
13
+ return parsed;
14
+ }
15
+ function escapeHtml(value) {
16
+ return String(value !== null && value !== void 0 ? value : '')
17
+ .replace(/&/g, '&amp;')
18
+ .replace(/</g, '&lt;')
19
+ .replace(/>/g, '&gt;')
20
+ .replace(/"/g, '&quot;')
21
+ .replace(/'/g, '&#39;');
22
+ }
23
+ export function QrSignupManager({ enabled = false, expiryDays = DEFAULT_EXPIRY_DAYS, }) {
8
24
  const { t } = useTranslation();
9
- const [label, setLabel] = useState('');
25
+ const qrWrapperRef = useRef(null);
10
26
  const [busy, setBusy] = useState(false);
11
27
  const [error, setError] = useState('');
12
28
  const [success, setSuccess] = useState('');
13
29
  const [result, setResult] = useState(null);
30
+ const [copyState, setCopyState] = useState('idle');
14
31
  const hasGeneratedRef = useRef(false);
15
- const generate = async () => {
32
+ const formattedExpiry = useMemo(() => {
33
+ if (!(result === null || result === void 0 ? void 0 : result.expires_at)) {
34
+ return '';
35
+ }
36
+ const parsed = new Date(result.expires_at);
37
+ if (Number.isNaN(parsed.getTime())) {
38
+ return result.expires_at;
39
+ }
40
+ return new Intl.DateTimeFormat(undefined, {
41
+ year: 'numeric',
42
+ month: '2-digit',
43
+ day: '2-digit',
44
+ hour: '2-digit',
45
+ minute: '2-digit',
46
+ }).format(parsed);
47
+ }, [result]);
48
+ const generate = async (daysOverride) => {
16
49
  if (!enabled) {
17
50
  setResult(null);
18
51
  return;
19
52
  }
53
+ const nextDays = clampExpiryDays(daysOverride !== null && daysOverride !== void 0 ? daysOverride : expiryDays);
20
54
  setBusy(true);
21
55
  setError('');
22
56
  setSuccess('');
57
+ setCopyState('idle');
23
58
  try {
24
59
  const data = await createSignupQr({
25
- label,
60
+ expires_minutes: nextDays * 24 * 60,
26
61
  });
27
62
  setResult(data);
28
63
  hasGeneratedRef.current = true;
29
- setSuccess(t('Auth.SIGNUP_QR_SAVE_SUCCESS', 'QR signup link updated.'));
64
+ setSuccess(t('Auth.SIGNUP_QR_CREATE_SUCCESS', 'New QR signup link created.'));
30
65
  }
31
66
  catch (err) {
32
67
  setError(t((err === null || err === void 0 ? void 0 : err.code) || 'Auth.SIGNUP_QR_CREATE_FAILED', 'Could not create signup QR.'));
@@ -40,6 +75,7 @@ export function QrSignupManager({ enabled = false }) {
40
75
  setResult(null);
41
76
  setError('');
42
77
  setSuccess('');
78
+ setCopyState('idle');
43
79
  hasGeneratedRef.current = false;
44
80
  return;
45
81
  }
@@ -51,15 +87,17 @@ export function QrSignupManager({ enabled = false }) {
51
87
  setBusy(true);
52
88
  setError('');
53
89
  setSuccess('');
90
+ setCopyState('idle');
54
91
  try {
92
+ const days = clampExpiryDays(expiryDays);
55
93
  const data = await createSignupQr({
56
- label,
94
+ expires_minutes: days * 24 * 60,
57
95
  });
58
96
  if (!active)
59
97
  return;
60
98
  setResult(data);
61
99
  hasGeneratedRef.current = true;
62
- setSuccess(t('Auth.SIGNUP_QR_SAVE_SUCCESS', 'QR signup link updated.'));
100
+ setSuccess(t('Auth.SIGNUP_QR_CREATE_SUCCESS', 'New QR signup link created.'));
63
101
  }
64
102
  catch (err) {
65
103
  if (!active)
@@ -76,6 +114,152 @@ export function QrSignupManager({ enabled = false }) {
76
114
  return () => {
77
115
  active = false;
78
116
  };
79
- }, [enabled, label, t]);
80
- 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', 'When QR signup is enabled, a signup QR code is shown here immediately.') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Auth.SIGNUP_QR_POLICY_HINT', 'The default QR validity is configured in the authentication policy above.') }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error }), success && _jsx(Alert, { severity: "success", sx: { mb: 2 }, children: success }), !enabled && (_jsx(Alert, { severity: "info", sx: { mb: 2 }, children: t('Auth.SIGNUP_QR_DISABLED_HINT', 'Enable self-signup by QR above to show a QR code here.') })), _jsx(TextField, { label: t('Common.LABEL', 'Label'), value: label, onChange: (event) => setLabel(event.target.value), fullWidth: true, disabled: !enabled || busy }), _jsx(Button, { variant: "contained", sx: { mt: 2 }, onClick: generate, disabled: !enabled || busy, children: t('Common.SAVE', 'Save') }), (result === null || result === void 0 ? void 0 : result.signup_url) && (_jsxs(Box, { sx: { mt: 3 }, children: [_jsx(QRCodeSVG, { value: result.signup_url, size: 180, includeMargin: true }), _jsx(TextField, { label: t('Auth.SIGNUP_QR_LINK_LABEL', 'Signup link'), value: result.signup_url, fullWidth: true, multiline: true, minRows: 2, sx: { mt: 2 }, InputProps: { readOnly: true } }), (result === null || result === void 0 ? void 0 : result.expires_at) && (_jsxs(Typography, { variant: "body2", sx: { mt: 1.5, color: 'text.secondary' }, children: [t('Auth.SIGNUP_QR_VALID_UNTIL', 'Valid until'), ": ", result.expires_at] }))] }))] }));
117
+ }, [enabled, expiryDays, t]);
118
+ const handleCopyLink = async () => {
119
+ var _a;
120
+ const signupUrl = result === null || result === void 0 ? void 0 : result.signup_url;
121
+ if (!signupUrl || !((_a = navigator === null || navigator === void 0 ? void 0 : navigator.clipboard) === null || _a === void 0 ? void 0 : _a.writeText)) {
122
+ setCopyState('error');
123
+ return;
124
+ }
125
+ try {
126
+ await navigator.clipboard.writeText(signupUrl);
127
+ setCopyState('copied');
128
+ }
129
+ catch (_error) {
130
+ setCopyState('error');
131
+ }
132
+ };
133
+ const handleSavePdf = () => {
134
+ var _a;
135
+ if (!(result === null || result === void 0 ? void 0 : result.signup_url)) {
136
+ return;
137
+ }
138
+ const svgMarkup = (_a = qrWrapperRef.current) === null || _a === void 0 ? void 0 : _a.innerHTML;
139
+ if (!svgMarkup) {
140
+ setError(t('Auth.SIGNUP_QR_PDF_NOT_READY', 'The QR image is not ready yet. Please try again.'));
141
+ return;
142
+ }
143
+ const printWindow = window.open('', '_blank', 'width=960,height=900');
144
+ if (!printWindow) {
145
+ setError(t('Auth.SIGNUP_QR_PDF_BLOCKED', 'Popup blocked. Please allow popups to save the QR card as PDF.'));
146
+ return;
147
+ }
148
+ const safeUrl = escapeHtml(result.signup_url);
149
+ const safeExpiresAt = escapeHtml(formattedExpiry || result.expires_at || '');
150
+ printWindow.document.write(`
151
+ <!DOCTYPE html>
152
+ <html>
153
+ <head>
154
+ <meta charset="utf-8" />
155
+ <title>${escapeHtml(t('Auth.SIGNUP_QR_MANAGER_TITLE', 'QR Signup'))}</title>
156
+ <style>
157
+ body {
158
+ margin: 0;
159
+ padding: 32px;
160
+ font-family: Arial, sans-serif;
161
+ background: #f5f7fb;
162
+ color: #122033;
163
+ }
164
+ .card {
165
+ max-width: 720px;
166
+ margin: 0 auto;
167
+ border: 1px solid #d9e2f2;
168
+ border-radius: 20px;
169
+ background: #ffffff;
170
+ padding: 32px;
171
+ box-sizing: border-box;
172
+ }
173
+ .eyebrow {
174
+ display: inline-block;
175
+ padding: 6px 10px;
176
+ border-radius: 999px;
177
+ background: #e8f0ff;
178
+ color: #23408e;
179
+ font-size: 12px;
180
+ font-weight: 700;
181
+ letter-spacing: 0.06em;
182
+ text-transform: uppercase;
183
+ }
184
+ h1 {
185
+ margin: 16px 0 8px;
186
+ font-size: 28px;
187
+ line-height: 1.2;
188
+ }
189
+ .qr-box {
190
+ display: flex;
191
+ justify-content: center;
192
+ align-items: center;
193
+ padding: 24px;
194
+ border-radius: 16px;
195
+ background: #ffffff;
196
+ border: 1px solid #d9e2f2;
197
+ }
198
+ .meta {
199
+ margin-top: 20px;
200
+ padding: 16px;
201
+ border-radius: 16px;
202
+ background: #f8faff;
203
+ border: 1px solid #d9e2f2;
204
+ word-break: break-word;
205
+ font-size: 14px;
206
+ line-height: 1.5;
207
+ }
208
+ @media print {
209
+ body {
210
+ background: #ffffff;
211
+ padding: 0;
212
+ }
213
+ .card {
214
+ border: 0;
215
+ border-radius: 0;
216
+ max-width: none;
217
+ padding: 0;
218
+ }
219
+ }
220
+ </style>
221
+ </head>
222
+ <body>
223
+ <div class="card">
224
+ <div class="eyebrow">${escapeHtml(t('Auth.SIGNUP_QR_MANAGER_TITLE', 'QR Signup'))}</div>
225
+ <h1>${escapeHtml(t('Auth.SIGNUP_QR_PRINT_TITLE', 'Sign-Up Access'))}</h1>
226
+ <div class="qr-box">${svgMarkup}</div>
227
+ <div class="meta">
228
+ <strong>${escapeHtml(t('Auth.SIGNUP_QR_LINK_LABEL', 'Signup link'))}</strong><br />
229
+ <a href="${safeUrl}">${safeUrl}</a><br /><br />
230
+ <strong>${escapeHtml(t('Auth.SIGNUP_QR_VALID_UNTIL', 'Valid until'))}</strong>: ${safeExpiresAt}
231
+ </div>
232
+ </div>
233
+ <script>
234
+ window.addEventListener('load', function () {
235
+ window.focus();
236
+ window.print();
237
+ });
238
+ </script>
239
+ </body>
240
+ </html>
241
+ `);
242
+ printWindow.document.close();
243
+ };
244
+ if (!enabled) {
245
+ return null;
246
+ }
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: {
248
+ display: 'flex',
249
+ justifyContent: 'center',
250
+ alignItems: 'center',
251
+ borderRadius: 3,
252
+ border: '1px solid',
253
+ borderColor: 'divider',
254
+ bgcolor: '#ffffff',
255
+ py: 3,
256
+ px: 2,
257
+ }, children: _jsx(Box, { ref: qrWrapperRef, children: _jsx(QRCodeSVG, { value: result.signup_url, size: 220, includeMargin: true }) }) }), _jsxs(Box, { sx: {
258
+ mt: 2,
259
+ borderRadius: 3,
260
+ p: 2,
261
+ bgcolor: 'grey.50',
262
+ border: '1px solid',
263
+ borderColor: 'divider',
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') })] })] }))] }));
81
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,8 +1,8 @@
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, Button, FormControlLabel, Stack, Switch, TextField, 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,
@@ -10,92 +10,44 @@ const EMPTY_POLICY = {
10
10
  allow_self_signup_email_domain: false,
11
11
  allow_self_signup_qr: false,
12
12
  allowed_email_domains: [],
13
- required_auth_factor_count: 1,
14
13
  signup_qr_expiry_days: 90,
14
+ required_auth_factor_count: 1,
15
15
  };
16
- export function RegistrationMethodsManager({ onPolicyChange }) {
16
+ export function RegistrationMethodsManager({ policy: authPolicy, error = '', onPolicyChange, }) {
17
17
  const { t } = useTranslation();
18
- const [policy, setPolicy] = useState(EMPTY_POLICY);
19
- const [domainsText, setDomainsText] = useState('');
20
- const [signupQrExpiryDays, setSignupQrExpiryDays] = useState(String(EMPTY_POLICY.signup_qr_expiry_days));
21
- const [busy, setBusy] = useState(false);
18
+ const [policyState, setPolicyState] = useState(EMPTY_POLICY);
22
19
  const [busyField, setBusyField] = useState('');
23
- const [error, setError] = useState('');
20
+ const [saveError, setSaveError] = useState('');
24
21
  const [success, setSuccess] = useState('');
25
22
  useEffect(() => {
26
- let active = true;
27
- (async () => {
28
- try {
29
- const data = await fetchAuthPolicy();
30
- if (!active)
31
- return;
32
- setPolicy((prev) => (Object.assign(Object.assign({}, prev), data)));
33
- setDomainsText(((data === null || data === void 0 ? void 0 : data.allowed_email_domains) || []).join('\n'));
34
- setSignupQrExpiryDays(String((data === null || data === void 0 ? void 0 : data.signup_qr_expiry_days) || EMPTY_POLICY.signup_qr_expiry_days));
35
- if (onPolicyChange)
36
- onPolicyChange(data);
37
- }
38
- catch (err) {
39
- if (active) {
40
- setError(t((err === null || err === void 0 ? void 0 : err.code) || 'Auth.AUTH_POLICY_FETCH_FAILED', 'Could not load authentication policy.'));
41
- }
42
- }
43
- })();
44
- return () => {
45
- active = false;
46
- };
47
- }, [onPolicyChange, t]);
23
+ if (!authPolicy) {
24
+ return;
25
+ }
26
+ setPolicyState(Object.assign(Object.assign({}, EMPTY_POLICY), authPolicy));
27
+ }, [authPolicy]);
48
28
  const toggle = (field) => async (_event, checked) => {
49
- const previous = policy[field];
50
- setPolicy((prev) => (Object.assign(Object.assign({}, prev), { [field]: checked })));
29
+ const previous = policyState[field];
30
+ setPolicyState((prev) => (Object.assign(Object.assign({}, prev), { [field]: checked })));
51
31
  setBusyField(field);
52
- setError('');
32
+ setSaveError('');
53
33
  setSuccess('');
54
34
  try {
55
35
  const next = await updateAuthPolicy({ [field]: checked });
56
- setPolicy((prev) => (Object.assign(Object.assign({}, prev), next)));
57
- setDomainsText(((next === null || next === void 0 ? void 0 : next.allowed_email_domains) || []).join('\n'));
36
+ setPolicyState((prev) => (Object.assign(Object.assign({}, prev), next)));
58
37
  setSuccess(t('Auth.AUTH_POLICY_SAVE_SUCCESS', 'Authentication settings saved.'));
59
38
  if (onPolicyChange)
60
39
  onPolicyChange(next);
61
40
  }
62
41
  catch (err) {
63
- setPolicy((prev) => (Object.assign(Object.assign({}, prev), { [field]: previous })));
64
- 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.'));
65
44
  }
66
45
  finally {
67
46
  setBusyField('');
68
47
  }
69
48
  };
70
- const save = async () => {
71
- setBusy(true);
72
- setError('');
73
- setSuccess('');
74
- try {
75
- const allowed_email_domains = domainsText
76
- .split(/\r?\n/)
77
- .map((value) => value.trim())
78
- .filter(Boolean);
79
- const parsedExpiryDays = parseInt(signupQrExpiryDays, 10);
80
- const next = await updateAuthPolicy({
81
- allowed_email_domains,
82
- signup_qr_expiry_days: Number.isFinite(parsedExpiryDays) && parsedExpiryDays > 0
83
- ? parsedExpiryDays
84
- : EMPTY_POLICY.signup_qr_expiry_days,
85
- });
86
- setPolicy((prev) => (Object.assign(Object.assign({}, prev), next)));
87
- setDomainsText(((next === null || next === void 0 ? void 0 : next.allowed_email_domains) || allowed_email_domains).join('\n'));
88
- setSignupQrExpiryDays(String((next === null || next === void 0 ? void 0 : next.signup_qr_expiry_days) || EMPTY_POLICY.signup_qr_expiry_days));
89
- setSuccess(t('Auth.AUTH_POLICY_SAVE_SUCCESS', 'Authentication settings saved.'));
90
- if (onPolicyChange)
91
- onPolicyChange(next);
92
- }
93
- catch (err) {
94
- setError(t((err === null || err === void 0 ? void 0 : err.code) || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save authentication settings.'));
95
- }
96
- finally {
97
- setBusy(false);
98
- }
99
- };
100
- 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.') })), _jsx(TextField, { label: t('Auth.ALLOWED_EMAIL_DOMAINS_LABEL', 'Allowed email domains'), helperText: t('Auth.ALLOWED_EMAIL_DOMAINS_HINT', 'One domain per line, e.g. example.org. You can leave this empty temporarily.'), multiline: true, minRows: 3, fullWidth: true, sx: { mt: 2 }, value: domainsText, onChange: (event) => setDomainsText(event.target.value) }), _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", fullWidth: true, sx: { mt: 2 }, value: signupQrExpiryDays, onChange: (event) => setSignupQrExpiryDays(event.target.value) }), _jsx(Button, { variant: "contained", sx: { mt: 2 }, onClick: save, disabled: busy, children: t('Common.SAVE', 'Save') })] }));
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.') }))] }));
101
53
  }