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

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.
@@ -92,7 +92,7 @@ export function SignUpPage() {
92
92
  const handleGoToLogin = () => {
93
93
  navigate('/login');
94
94
  };
95
- return (_jsxs(NarrowPage, { title: t('Auth.LOGIN_SIGNUP_BUTTON'), subtitle: pageSubtitle, children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t('Auth.LOGIN_SIGNUP_BUTTON')] }) }), successKey && (_jsx(Alert, { severity: "success", sx: { mb: 2 }, children: t(successKey, { email }) })), errorKey && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(errorKey, t('Auth.INVITE_FAILED', 'Could not complete signup.')) })), signupModes.length > 1 && (_jsx(Stack, { spacing: 1, sx: { mb: 2 }, children: _jsx(Stack, { spacing: 1, children: signupModes.map((entry) => (_jsx(Button, { variant: mode === entry ? 'contained' : 'outlined', onClick: () => setMode(entry), disabled: submitting, fullWidth: true, children: t(MODE_LABELS[entry] || entry, entry) }, entry))) }) })), _jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { display: 'flex', flexDirection: 'column', gap: 2 }, children: [_jsx(TextField, { label: t('Auth.EMAIL_LABEL'), type: "email", required: true, fullWidth: true, value: email, onChange: (e) => setEmail(e.target.value), disabled: submitting }), mode === 'self_signup_access_code' && (_jsx(TextField, { label: t('Auth.ACCESS_CODE_LABEL'), type: "text", required: true, fullWidth: true, value: accessCode, onChange: (e) => setAccessCode(e.target.value), disabled: submitting })), mode === 'self_signup_qr' && (_jsx(Stack, { spacing: 1, children: _jsx(TextField, { label: t('Auth.SIGNUP_QR_TOKEN_LABEL', 'QR token'), value: tokenFromUrl, fullWidth: true, InputProps: { readOnly: true } }) })), _jsx(Button, { type: "submit", variant: "contained", disabled: submitting || signupModes.length === 0, children: submitting
95
+ return (_jsxs(NarrowPage, { title: t('Auth.LOGIN_SIGNUP_BUTTON'), subtitle: pageSubtitle, children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t('Auth.LOGIN_SIGNUP_BUTTON')] }) }), successKey && (_jsx(Alert, { severity: "success", sx: { mb: 2 }, children: t(successKey, { email }) })), errorKey && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(errorKey, t('Auth.INVITE_FAILED', 'Could not complete signup.')) })), signupModes.length > 1 && (_jsx(Stack, { spacing: 1, sx: { mb: 2 }, children: _jsx(Stack, { spacing: 1, children: signupModes.map((entry) => (_jsx(Button, { variant: mode === entry ? 'contained' : 'outlined', onClick: () => setMode(entry), disabled: submitting, fullWidth: true, children: t(MODE_LABELS[entry] || entry, entry) }, entry))) }) })), _jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { display: 'flex', flexDirection: 'column', gap: 2 }, children: [_jsx(TextField, { label: t('Auth.EMAIL_LABEL'), type: "email", required: true, fullWidth: true, value: email, onChange: (e) => setEmail(e.target.value), disabled: submitting }), mode === 'self_signup_access_code' && (_jsx(TextField, { label: t('Auth.ACCESS_CODE_LABEL'), type: "text", required: true, fullWidth: true, value: accessCode, onChange: (e) => setAccessCode(e.target.value), disabled: submitting })), _jsx(Button, { type: "submit", variant: "contained", disabled: submitting || signupModes.length === 0, children: submitting
96
96
  ? t('Auth.SIGNUP_SUBMITTING')
97
97
  : t('Auth.SIGNUP_SUBMIT') })] }), _jsx(Box, { sx: { mt: 3 }, children: _jsxs(Typography, { variant: "body2", children: [t('Auth.SIGNUP_ALREADY_HAVE_ACCOUNT'), ' ', _jsx(Button, { onClick: handleGoToLogin, variant: "text", size: "small", children: t('Auth.SIGNUP_GO_TO_LOGIN') })] }) })] }));
98
98
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "2.2.2",
3
+ "version": "2.2.4",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "private": false,
@@ -0,0 +1,87 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import {
3
+ Alert,
4
+ Box,
5
+ Button,
6
+ TextField,
7
+ Typography,
8
+ } from '@mui/material';
9
+ import { useTranslation } from 'react-i18next';
10
+ import { updateAuthPolicy } from '../auth/authApi';
11
+
12
+ export function AllowedEmailDomainsManager({
13
+ domains = [],
14
+ enabled = false,
15
+ onPolicyChange,
16
+ }) {
17
+ const { t } = useTranslation();
18
+ const [domainsText, setDomainsText] = useState('');
19
+ const [busy, setBusy] = useState(false);
20
+ const [error, setError] = useState('');
21
+ const [success, setSuccess] = useState('');
22
+
23
+ useEffect(() => {
24
+ setDomainsText((domains || []).join('\n'));
25
+ }, [domains]);
26
+
27
+ const handleSave = async () => {
28
+ setBusy(true);
29
+ setError('');
30
+ setSuccess('');
31
+ try {
32
+ const allowedEmailDomains = domainsText
33
+ .split(/\r?\n/)
34
+ .map((value) => value.trim())
35
+ .filter(Boolean);
36
+ const next = await updateAuthPolicy({
37
+ allowed_email_domains: allowedEmailDomains,
38
+ });
39
+ setDomainsText((next?.allowed_email_domains || allowedEmailDomains).join('\n'));
40
+ setSuccess(t('Auth.AUTH_POLICY_SAVE_SUCCESS', 'Authentication settings saved.'));
41
+ if (onPolicyChange) onPolicyChange(next);
42
+ } catch (err) {
43
+ setError(t(err?.code || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save authentication settings.'));
44
+ } finally {
45
+ setBusy(false);
46
+ }
47
+ };
48
+
49
+ if (!enabled) {
50
+ return null;
51
+ }
52
+
53
+ return (
54
+ <Box>
55
+ <Typography variant="h6" gutterBottom>
56
+ {t('Auth.ALLOWED_EMAIL_DOMAINS_TITLE', 'Allowed Email Domains')}
57
+ </Typography>
58
+ <Typography variant="body2" sx={{ mb: 2, color: 'text.secondary' }}>
59
+ {t(
60
+ 'Auth.ALLOWED_EMAIL_DOMAINS_CARD_HINT',
61
+ 'Only addresses from these domains can use email-domain sign-up.',
62
+ )}
63
+ </Typography>
64
+
65
+ {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
66
+ {success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
67
+
68
+ <TextField
69
+ label={t('Auth.ALLOWED_EMAIL_DOMAINS_LABEL', 'Allowed email domains')}
70
+ helperText={t(
71
+ 'Auth.ALLOWED_EMAIL_DOMAINS_HINT',
72
+ 'One domain per line, e.g. example.org. You can leave this empty temporarily.',
73
+ )}
74
+ multiline
75
+ minRows={4}
76
+ fullWidth
77
+ value={domainsText}
78
+ onChange={(event) => setDomainsText(event.target.value)}
79
+ disabled={busy}
80
+ />
81
+
82
+ <Button variant="contained" sx={{ mt: 2 }} onClick={handleSave} disabled={busy}>
83
+ {t('Common.SAVE', 'Save')}
84
+ </Button>
85
+ </Box>
86
+ );
87
+ }
@@ -1,39 +1,89 @@
1
- import React, { useEffect, useRef, useState } from 'react';
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
2
2
  import {
3
3
  Alert,
4
4
  Box,
5
5
  Button,
6
+ Stack,
6
7
  TextField,
7
8
  Typography,
8
9
  } from '@mui/material';
9
10
  import { useTranslation } from 'react-i18next';
10
11
  import { QRCodeSVG } from 'qrcode.react';
11
- import { createSignupQr } from '../auth/authApi';
12
+ import { createSignupQr, updateAuthPolicy } from '../auth/authApi';
12
13
 
13
- export function QrSignupManager({ enabled = false }) {
14
+ const DEFAULT_EXPIRY_DAYS = 90;
15
+
16
+ function clampExpiryDays(value) {
17
+ const parsed = parseInt(value, 10);
18
+ if (!Number.isFinite(parsed) || parsed < 1) {
19
+ return DEFAULT_EXPIRY_DAYS;
20
+ }
21
+ return parsed;
22
+ }
23
+
24
+ function escapeHtml(value) {
25
+ return String(value ?? '')
26
+ .replace(/&/g, '&amp;')
27
+ .replace(/</g, '&lt;')
28
+ .replace(/>/g, '&gt;')
29
+ .replace(/"/g, '&quot;')
30
+ .replace(/'/g, '&#39;');
31
+ }
32
+
33
+ export function QrSignupManager({
34
+ enabled = false,
35
+ expiryDays = DEFAULT_EXPIRY_DAYS,
36
+ onPolicyChange,
37
+ }) {
14
38
  const { t } = useTranslation();
15
- const [label, setLabel] = useState('');
39
+ const qrWrapperRef = useRef(null);
40
+ const [currentExpiryDays, setCurrentExpiryDays] = useState(String(expiryDays || DEFAULT_EXPIRY_DAYS));
16
41
  const [busy, setBusy] = useState(false);
42
+ const [savingPolicy, setSavingPolicy] = useState(false);
17
43
  const [error, setError] = useState('');
18
44
  const [success, setSuccess] = useState('');
19
45
  const [result, setResult] = useState(null);
46
+ const [copyState, setCopyState] = useState('idle');
20
47
  const hasGeneratedRef = useRef(false);
21
48
 
22
- const generate = async () => {
49
+ useEffect(() => {
50
+ setCurrentExpiryDays(String(expiryDays || DEFAULT_EXPIRY_DAYS));
51
+ }, [expiryDays]);
52
+
53
+ const formattedExpiry = useMemo(() => {
54
+ if (!result?.expires_at) {
55
+ return '';
56
+ }
57
+ const parsed = new Date(result.expires_at);
58
+ if (Number.isNaN(parsed.getTime())) {
59
+ return result.expires_at;
60
+ }
61
+ return new Intl.DateTimeFormat(undefined, {
62
+ year: 'numeric',
63
+ month: '2-digit',
64
+ day: '2-digit',
65
+ hour: '2-digit',
66
+ minute: '2-digit',
67
+ }).format(parsed);
68
+ }, [result]);
69
+
70
+ const generate = async (daysOverride) => {
23
71
  if (!enabled) {
24
72
  setResult(null);
25
73
  return;
26
74
  }
75
+ const nextDays = clampExpiryDays(daysOverride ?? currentExpiryDays);
27
76
  setBusy(true);
28
77
  setError('');
29
78
  setSuccess('');
79
+ setCopyState('idle');
30
80
  try {
31
81
  const data = await createSignupQr({
32
- label,
82
+ expires_minutes: nextDays * 24 * 60,
33
83
  });
34
84
  setResult(data);
35
85
  hasGeneratedRef.current = true;
36
- setSuccess(t('Auth.SIGNUP_QR_SAVE_SUCCESS', 'QR signup link updated.'));
86
+ setSuccess(t('Auth.SIGNUP_QR_CREATE_SUCCESS', 'New QR signup link created.'));
37
87
  } catch (err) {
38
88
  setError(t(err?.code || 'Auth.SIGNUP_QR_CREATE_FAILED', 'Could not create signup QR.'));
39
89
  } finally {
@@ -46,6 +96,7 @@ export function QrSignupManager({ enabled = false }) {
46
96
  setResult(null);
47
97
  setError('');
48
98
  setSuccess('');
99
+ setCopyState('idle');
49
100
  hasGeneratedRef.current = false;
50
101
  return;
51
102
  }
@@ -57,14 +108,16 @@ export function QrSignupManager({ enabled = false }) {
57
108
  setBusy(true);
58
109
  setError('');
59
110
  setSuccess('');
111
+ setCopyState('idle');
60
112
  try {
113
+ const days = clampExpiryDays(expiryDays);
61
114
  const data = await createSignupQr({
62
- label,
115
+ expires_minutes: days * 24 * 60,
63
116
  });
64
117
  if (!active) return;
65
118
  setResult(data);
66
119
  hasGeneratedRef.current = true;
67
- setSuccess(t('Auth.SIGNUP_QR_SAVE_SUCCESS', 'QR signup link updated.'));
120
+ setSuccess(t('Auth.SIGNUP_QR_CREATE_SUCCESS', 'New QR signup link created.'));
68
121
  } catch (err) {
69
122
  if (!active) return;
70
123
  setError(t(err?.code || 'Auth.SIGNUP_QR_CREATE_FAILED', 'Could not create signup QR.'));
@@ -78,7 +131,158 @@ export function QrSignupManager({ enabled = false }) {
78
131
  return () => {
79
132
  active = false;
80
133
  };
81
- }, [enabled, label, t]);
134
+ }, [enabled, expiryDays, t]);
135
+
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
+ const handleCopyLink = async () => {
156
+ const signupUrl = result?.signup_url;
157
+ if (!signupUrl || !navigator?.clipboard?.writeText) {
158
+ setCopyState('error');
159
+ return;
160
+ }
161
+ try {
162
+ await navigator.clipboard.writeText(signupUrl);
163
+ setCopyState('copied');
164
+ } catch (_error) {
165
+ setCopyState('error');
166
+ }
167
+ };
168
+
169
+ const handleSavePdf = () => {
170
+ if (!result?.signup_url) {
171
+ return;
172
+ }
173
+ const svgMarkup = qrWrapperRef.current?.innerHTML;
174
+ if (!svgMarkup) {
175
+ setError(t('Auth.SIGNUP_QR_PDF_NOT_READY', 'The QR image is not ready yet. Please try again.'));
176
+ return;
177
+ }
178
+
179
+ const printWindow = window.open('', '_blank', 'width=960,height=900');
180
+ if (!printWindow) {
181
+ setError(t('Auth.SIGNUP_QR_PDF_BLOCKED', 'Popup blocked. Please allow popups to save the QR card as PDF.'));
182
+ return;
183
+ }
184
+
185
+ const safeUrl = escapeHtml(result.signup_url);
186
+ const safeExpiresAt = escapeHtml(formattedExpiry || result.expires_at || '');
187
+
188
+ printWindow.document.write(`
189
+ <!DOCTYPE html>
190
+ <html>
191
+ <head>
192
+ <meta charset="utf-8" />
193
+ <title>${escapeHtml(t('Auth.SIGNUP_QR_MANAGER_TITLE', 'QR Signup'))}</title>
194
+ <style>
195
+ body {
196
+ margin: 0;
197
+ padding: 32px;
198
+ font-family: Arial, sans-serif;
199
+ background: #f5f7fb;
200
+ color: #122033;
201
+ }
202
+ .card {
203
+ max-width: 720px;
204
+ margin: 0 auto;
205
+ border: 1px solid #d9e2f2;
206
+ border-radius: 20px;
207
+ background: #ffffff;
208
+ padding: 32px;
209
+ box-sizing: border-box;
210
+ }
211
+ .eyebrow {
212
+ display: inline-block;
213
+ padding: 6px 10px;
214
+ border-radius: 999px;
215
+ background: #e8f0ff;
216
+ color: #23408e;
217
+ font-size: 12px;
218
+ font-weight: 700;
219
+ letter-spacing: 0.06em;
220
+ text-transform: uppercase;
221
+ }
222
+ h1 {
223
+ margin: 16px 0 8px;
224
+ font-size: 28px;
225
+ line-height: 1.2;
226
+ }
227
+ .qr-box {
228
+ display: flex;
229
+ justify-content: center;
230
+ align-items: center;
231
+ padding: 24px;
232
+ border-radius: 16px;
233
+ background: #ffffff;
234
+ border: 1px solid #d9e2f2;
235
+ }
236
+ .meta {
237
+ margin-top: 20px;
238
+ padding: 16px;
239
+ border-radius: 16px;
240
+ background: #f8faff;
241
+ border: 1px solid #d9e2f2;
242
+ word-break: break-word;
243
+ font-size: 14px;
244
+ line-height: 1.5;
245
+ }
246
+ @media print {
247
+ body {
248
+ background: #ffffff;
249
+ padding: 0;
250
+ }
251
+ .card {
252
+ border: 0;
253
+ border-radius: 0;
254
+ max-width: none;
255
+ padding: 0;
256
+ }
257
+ }
258
+ </style>
259
+ </head>
260
+ <body>
261
+ <div class="card">
262
+ <div class="eyebrow">${escapeHtml(t('Auth.SIGNUP_QR_MANAGER_TITLE', 'QR Signup'))}</div>
263
+ <h1>${escapeHtml(t('Auth.SIGNUP_QR_PRINT_TITLE', 'Sign-Up Access'))}</h1>
264
+ <div class="qr-box">${svgMarkup}</div>
265
+ <div class="meta">
266
+ <strong>${escapeHtml(t('Auth.SIGNUP_QR_LINK_LABEL', 'Signup link'))}</strong><br />
267
+ <a href="${safeUrl}">${safeUrl}</a><br /><br />
268
+ <strong>${escapeHtml(t('Auth.SIGNUP_QR_VALID_UNTIL', 'Valid until'))}</strong>: ${safeExpiresAt}
269
+ </div>
270
+ </div>
271
+ <script>
272
+ window.addEventListener('load', function () {
273
+ window.focus();
274
+ window.print();
275
+ });
276
+ </script>
277
+ </body>
278
+ </html>
279
+ `);
280
+ printWindow.document.close();
281
+ };
282
+
283
+ if (!enabled) {
284
+ return null;
285
+ }
82
286
 
83
287
  return (
84
288
  <Box>
@@ -86,41 +290,94 @@ export function QrSignupManager({ enabled = false }) {
86
290
  {t('Auth.SIGNUP_QR_MANAGER_TITLE', 'QR Signup')}
87
291
  </Typography>
88
292
  <Typography variant="body2" sx={{ mb: 2, color: 'text.secondary' }}>
89
- {t('Auth.SIGNUP_QR_MANAGER_HINT', 'When QR signup is enabled, a signup QR code is shown here immediately.')}
293
+ {t('Auth.SIGNUP_QR_MANAGER_HINT', 'Save the default validity, then generate and share QR signup links below.')}
90
294
  </Typography>
295
+
91
296
  {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
92
297
  {success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
93
-
94
- {!enabled && (
95
- <Alert severity="info" sx={{ mb: 2 }}>
96
- {t('Auth.SIGNUP_QR_DISABLED_HINT', 'Enable self-signup by QR above to show a QR code here.')}
298
+ {copyState === 'copied' && (
299
+ <Alert severity="success" sx={{ mb: 2 }}>
300
+ {t('Auth.SIGNUP_QR_LINK_COPIED', 'Signup link copied.')}
301
+ </Alert>
302
+ )}
303
+ {copyState === 'error' && (
304
+ <Alert severity="warning" sx={{ mb: 2 }}>
305
+ {t('Auth.SIGNUP_QR_COPY_UNAVAILABLE', 'Copying the link is not available in this browser.')}
97
306
  </Alert>
98
307
  )}
99
308
 
100
- <TextField
101
- label={t('Common.LABEL', 'Label')}
102
- value={label}
103
- onChange={(event) => setLabel(event.target.value)}
104
- fullWidth
105
- disabled={!enabled || busy}
106
- />
107
-
108
- <Button variant="contained" sx={{ mt: 2 }} onClick={generate} disabled={!enabled || busy}>
109
- {t('Common.SAVE', 'Save')}
110
- </Button>
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>
111
331
 
112
332
  {result?.signup_url && (
113
333
  <Box sx={{ mt: 3 }}>
114
- <QRCodeSVG value={result.signup_url} size={180} includeMargin />
115
- <TextField
116
- label={t('Auth.SIGNUP_QR_LINK_LABEL', 'Signup link')}
117
- value={result.signup_url}
118
- fullWidth
119
- multiline
120
- minRows={2}
121
- sx={{ mt: 2 }}
122
- InputProps={{ readOnly: true }}
123
- />
334
+ <Box
335
+ sx={{
336
+ display: 'flex',
337
+ justifyContent: 'center',
338
+ alignItems: 'center',
339
+ borderRadius: 3,
340
+ border: '1px solid',
341
+ borderColor: 'divider',
342
+ bgcolor: '#ffffff',
343
+ py: 3,
344
+ px: 2,
345
+ }}
346
+ >
347
+ <Box ref={qrWrapperRef}>
348
+ <QRCodeSVG value={result.signup_url} size={220} includeMargin />
349
+ </Box>
350
+ </Box>
351
+
352
+ <Box
353
+ sx={{
354
+ mt: 2,
355
+ borderRadius: 3,
356
+ p: 2,
357
+ bgcolor: 'grey.50',
358
+ border: '1px solid',
359
+ borderColor: 'divider',
360
+ }}
361
+ >
362
+ <Typography variant="subtitle2" gutterBottom>
363
+ {t('Auth.SIGNUP_QR_ACCESS_TITLE', 'Signup Access')}
364
+ </Typography>
365
+ <Typography variant="body2" color="text.secondary">
366
+ {t('Auth.SIGNUP_QR_VALID_UNTIL', 'Valid until')}: {formattedExpiry || result.expires_at}
367
+ </Typography>
368
+ </Box>
369
+
370
+ <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1} sx={{ mt: 2 }}>
371
+ <Button variant="outlined" onClick={() => generate()} disabled={busy || savingPolicy}>
372
+ {t('Auth.SIGNUP_QR_NEW_BUTTON', 'New QR-Code')}
373
+ </Button>
374
+ <Button variant="outlined" onClick={handleCopyLink} disabled={!result?.signup_url || busy}>
375
+ {t('Auth.SIGNUP_QR_COPY_BUTTON', 'Copy Link')}
376
+ </Button>
377
+ <Button variant="outlined" onClick={handleSavePdf} disabled={!result?.signup_url || busy}>
378
+ {t('Auth.SIGNUP_QR_PDF_BUTTON', 'Save as PDF')}
379
+ </Button>
380
+ </Stack>
124
381
  </Box>
125
382
  )}
126
383
  </Box>
@@ -2,11 +2,9 @@ import React, { useEffect, useState } from 'react';
2
2
  import {
3
3
  Alert,
4
4
  Box,
5
- Button,
6
5
  FormControlLabel,
7
6
  Stack,
8
7
  Switch,
9
- TextField,
10
8
  Typography,
11
9
  } from '@mui/material';
12
10
  import { useTranslation } from 'react-i18next';
@@ -18,15 +16,12 @@ const EMPTY_POLICY = {
18
16
  allow_self_signup_open: false,
19
17
  allow_self_signup_email_domain: false,
20
18
  allow_self_signup_qr: false,
21
- allowed_email_domains: [],
22
19
  required_auth_factor_count: 1,
23
20
  };
24
21
 
25
22
  export function RegistrationMethodsManager({ onPolicyChange }) {
26
23
  const { t } = useTranslation();
27
24
  const [policy, setPolicy] = useState(EMPTY_POLICY);
28
- const [domainsText, setDomainsText] = useState('');
29
- const [busy, setBusy] = useState(false);
30
25
  const [busyField, setBusyField] = useState('');
31
26
  const [error, setError] = useState('');
32
27
  const [success, setSuccess] = useState('');
@@ -38,7 +33,6 @@ export function RegistrationMethodsManager({ onPolicyChange }) {
38
33
  const data = await fetchAuthPolicy();
39
34
  if (!active) return;
40
35
  setPolicy((prev) => ({ ...prev, ...data }));
41
- setDomainsText((data?.allowed_email_domains || []).join('\n'));
42
36
  if (onPolicyChange) onPolicyChange(data);
43
37
  } catch (err) {
44
38
  if (active) {
@@ -60,7 +54,6 @@ export function RegistrationMethodsManager({ onPolicyChange }) {
60
54
  try {
61
55
  const next = await updateAuthPolicy({ [field]: checked });
62
56
  setPolicy((prev) => ({ ...prev, ...next }));
63
- setDomainsText((next?.allowed_email_domains || []).join('\n'));
64
57
  setSuccess(t('Auth.AUTH_POLICY_SAVE_SUCCESS', 'Authentication settings saved.'));
65
58
  if (onPolicyChange) onPolicyChange(next);
66
59
  } catch (err) {
@@ -71,27 +64,6 @@ export function RegistrationMethodsManager({ onPolicyChange }) {
71
64
  }
72
65
  };
73
66
 
74
- const save = async () => {
75
- setBusy(true);
76
- setError('');
77
- setSuccess('');
78
- try {
79
- const allowed_email_domains = domainsText
80
- .split(/\r?\n/)
81
- .map((value) => value.trim())
82
- .filter(Boolean);
83
- const next = await updateAuthPolicy({ allowed_email_domains });
84
- setPolicy((prev) => ({ ...prev, ...next }));
85
- setDomainsText((next?.allowed_email_domains || allowed_email_domains).join('\n'));
86
- setSuccess(t('Auth.AUTH_POLICY_SAVE_SUCCESS', 'Authentication settings saved.'));
87
- if (onPolicyChange) onPolicyChange(next);
88
- } catch (err) {
89
- setError(t(err?.code || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save authentication settings.'));
90
- } finally {
91
- setBusy(false);
92
- }
93
- };
94
-
95
67
  return (
96
68
  <Box>
97
69
  <Typography variant="h6" gutterBottom>
@@ -164,21 +136,6 @@ export function RegistrationMethodsManager({ onPolicyChange }) {
164
136
  )}
165
137
  </Alert>
166
138
  )}
167
-
168
- <TextField
169
- label={t('Auth.ALLOWED_EMAIL_DOMAINS_LABEL', 'Allowed email domains')}
170
- helperText={t('Auth.ALLOWED_EMAIL_DOMAINS_HINT', 'One domain per line, e.g. example.org. You can leave this empty temporarily.')}
171
- multiline
172
- minRows={3}
173
- fullWidth
174
- sx={{ mt: 2 }}
175
- value={domainsText}
176
- onChange={(event) => setDomainsText(event.target.value)}
177
- />
178
-
179
- <Button variant="contained" sx={{ mt: 2 }} onClick={save} disabled={busy}>
180
- {t('Common.SAVE', 'Save')}
181
- </Button>
182
139
  </Box>
183
140
  );
184
141
  }