@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.
- package/dist/components/AllowedEmailDomainsManager.js +43 -0
- package/dist/components/QrSignupManager.js +195 -11
- package/dist/components/QrSignupValidityManager.js +48 -0
- package/dist/components/RegistrationMethodsManager.js +21 -69
- package/dist/i18n/authTranslations.js +234 -0
- package/dist/pages/AccountPage.js +34 -3
- package/package.json +1 -1
- package/src/components/AllowedEmailDomainsManager.jsx +87 -0
- package/src/components/QrSignupManager.jsx +244 -45
- package/src/components/QrSignupValidityManager.jsx +100 -0
- package/src/components/RegistrationMethodsManager.jsx +35 -97
- package/src/i18n/authTranslations.ts +234 -0
- package/src/pages/AccountPage.jsx +73 -8
|
@@ -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,
|
|
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
|
-
|
|
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, '&')
|
|
18
|
+
.replace(/</g, '<')
|
|
19
|
+
.replace(/>/g, '>')
|
|
20
|
+
.replace(/"/g, '"')
|
|
21
|
+
.replace(/'/g, ''');
|
|
22
|
+
}
|
|
23
|
+
export function QrSignupManager({ enabled = false, expiryDays = DEFAULT_EXPIRY_DAYS, }) {
|
|
8
24
|
const { t } = useTranslation();
|
|
9
|
-
const
|
|
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
|
|
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
|
-
|
|
60
|
+
expires_minutes: nextDays * 24 * 60,
|
|
26
61
|
});
|
|
27
62
|
setResult(data);
|
|
28
63
|
hasGeneratedRef.current = true;
|
|
29
|
-
setSuccess(t('Auth.
|
|
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
|
-
|
|
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.
|
|
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,
|
|
80
|
-
|
|
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,
|
|
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,
|
|
@@ -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 [
|
|
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 [
|
|
20
|
+
const [saveError, setSaveError] = useState('');
|
|
24
21
|
const [success, setSuccess] = useState('');
|
|
25
22
|
useEffect(() => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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 =
|
|
50
|
-
|
|
29
|
+
const previous = policyState[field];
|
|
30
|
+
setPolicyState((prev) => (Object.assign(Object.assign({}, prev), { [field]: checked })));
|
|
51
31
|
setBusyField(field);
|
|
52
|
-
|
|
32
|
+
setSaveError('');
|
|
53
33
|
setSuccess('');
|
|
54
34
|
try {
|
|
55
35
|
const next = await updateAuthPolicy({ [field]: checked });
|
|
56
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
}
|