@micha.bigler/ui-core-micha 2.2.1 → 2.2.3

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.
@@ -77,5 +77,5 @@ export function QrSignupManager({ enabled = false }) {
77
77
  active = false;
78
78
  };
79
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.') }), 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 } })] }))] }));
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] }))] }))] }));
81
81
  }
@@ -11,11 +11,13 @@ const EMPTY_POLICY = {
11
11
  allow_self_signup_qr: false,
12
12
  allowed_email_domains: [],
13
13
  required_auth_factor_count: 1,
14
+ signup_qr_expiry_days: 90,
14
15
  };
15
16
  export function RegistrationMethodsManager({ onPolicyChange }) {
16
17
  const { t } = useTranslation();
17
18
  const [policy, setPolicy] = useState(EMPTY_POLICY);
18
19
  const [domainsText, setDomainsText] = useState('');
20
+ const [signupQrExpiryDays, setSignupQrExpiryDays] = useState(String(EMPTY_POLICY.signup_qr_expiry_days));
19
21
  const [busy, setBusy] = useState(false);
20
22
  const [busyField, setBusyField] = useState('');
21
23
  const [error, setError] = useState('');
@@ -29,6 +31,7 @@ export function RegistrationMethodsManager({ onPolicyChange }) {
29
31
  return;
30
32
  setPolicy((prev) => (Object.assign(Object.assign({}, prev), data)));
31
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));
32
35
  if (onPolicyChange)
33
36
  onPolicyChange(data);
34
37
  }
@@ -73,9 +76,16 @@ export function RegistrationMethodsManager({ onPolicyChange }) {
73
76
  .split(/\r?\n/)
74
77
  .map((value) => value.trim())
75
78
  .filter(Boolean);
76
- const next = await updateAuthPolicy({ allowed_email_domains });
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
+ });
77
86
  setPolicy((prev) => (Object.assign(Object.assign({}, prev), next)));
78
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));
79
89
  setSuccess(t('Auth.AUTH_POLICY_SAVE_SUCCESS', 'Authentication settings saved.'));
80
90
  if (onPolicyChange)
81
91
  onPolicyChange(next);
@@ -87,5 +97,5 @@ export function RegistrationMethodsManager({ onPolicyChange }) {
87
97
  setBusy(false);
88
98
  }
89
99
  };
90
- 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(Button, { variant: "contained", sx: { mt: 2 }, onClick: save, disabled: busy, children: t('Common.SAVE', 'Save') })] }));
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') })] }));
91
101
  }
@@ -295,10 +295,76 @@ export const authTranslations = {
295
295
  "sw": "Tafadhali ingiza anwani ya barua pepe."
296
296
  },
297
297
  "Auth.PAGE_SIGNUP_SUBTITLE": {
298
- "de": "Geben Sie Ihre E-Mail-Adresse und den Zugangscode ein, um eine Einladung anzufordern.",
299
- "fr": "Saisissez votre adresse e-mail et le code d'accès pour demander une invitation.",
300
- "en": "Enter your email address and access code to request an invitation.",
301
- "sw": "Ingiza barua pepe yako na msimbo wa ufikiaji ili kuomba mwaliko."
298
+ "de": "Wählen Sie eine Registrierungsart und geben Sie die benötigten Angaben ein.",
299
+ "fr": "Choisissez une méthode d'inscription et saisissez les informations requises.",
300
+ "en": "Choose a sign-up method and enter the required details.",
301
+ "sw": "Chagua njia ya kujisajili na uingize taarifa zinazohitajika."
302
+ },
303
+ "Auth.PAGE_SIGNUP_SUBTITLE_ACCESS_CODE": {
304
+ "de": "Geben Sie Ihre E-Mail-Adresse und den erhaltenen Zugangscode ein.",
305
+ "fr": "Saisissez votre adresse e-mail et le code d'accès reçu.",
306
+ "en": "Enter your email address and the access code you received.",
307
+ "sw": "Ingiza anwani yako ya barua pepe na msimbo wa ufikiaji uliopokea."
308
+ },
309
+ "Auth.PAGE_SIGNUP_SUBTITLE_OPEN": {
310
+ "de": "Geben Sie Ihre E-Mail-Adresse ein, um sich direkt zu registrieren.",
311
+ "fr": "Saisissez votre adresse e-mail pour vous inscrire directement.",
312
+ "en": "Enter your email address to sign up directly.",
313
+ "sw": "Ingiza anwani yako ya barua pepe ili kujisajili moja kwa moja."
314
+ },
315
+ "Auth.PAGE_SIGNUP_SUBTITLE_EMAIL_DOMAIN": {
316
+ "de": "Verwenden Sie eine E-Mail-Adresse aus einer freigegebenen Domain.",
317
+ "fr": "Utilisez une adresse e-mail provenant d'un domaine autorisé.",
318
+ "en": "Use an email address from an approved domain.",
319
+ "sw": "Tumia anwani ya barua pepe kutoka kwenye domeni iliyoidhinishwa."
320
+ },
321
+ "Auth.PAGE_SIGNUP_SUBTITLE_QR": {
322
+ "de": "Öffnen Sie diese Seite über einen gültigen QR-Link und geben Sie dann Ihre E-Mail-Adresse ein.",
323
+ "fr": "Ouvrez cette page via un lien QR valide, puis saisissez votre adresse e-mail.",
324
+ "en": "Open this page from a valid QR link, then enter your email address.",
325
+ "sw": "Fungua ukurasa huu kupitia kiungo halali cha QR, kisha ingiza anwani yako ya barua pepe."
326
+ },
327
+ "Auth.PAGE_SIGNUP_SUBTITLE_QR_READY": {
328
+ "de": "Geben Sie Ihre E-Mail-Adresse ein, um die Registrierung mit diesem QR-Link abzuschließen.",
329
+ "fr": "Saisissez votre adresse e-mail pour terminer l'inscription avec ce lien QR.",
330
+ "en": "Enter your email address to complete sign-up with this QR link.",
331
+ "sw": "Ingiza anwani yako ya barua pepe ili kukamilisha kujisajili kwa kutumia kiungo hiki cha QR."
332
+ },
333
+ "Auth.SIGNUP_ACCESS_CODE_TAB": {
334
+ "de": "Mit Zugangscode",
335
+ "fr": "Avec code d'accès",
336
+ "en": "With access code",
337
+ "sw": "Kwa msimbo wa ufikiaji"
338
+ },
339
+ "Auth.SIGNUP_OPEN_TAB": {
340
+ "de": "Direkte Registrierung",
341
+ "fr": "Inscription directe",
342
+ "en": "Direct sign-up",
343
+ "sw": "Kujisajili moja kwa moja"
344
+ },
345
+ "Auth.SIGNUP_EMAIL_DOMAIN_TAB": {
346
+ "de": "Mit E-Mail-Domain",
347
+ "fr": "Avec domaine e-mail",
348
+ "en": "With email domain",
349
+ "sw": "Kwa domeni ya barua pepe"
350
+ },
351
+ "Auth.SIGNUP_QR_TAB": {
352
+ "de": "Mit QR-Link",
353
+ "fr": "Avec lien QR",
354
+ "en": "With QR link",
355
+ "sw": "Kwa kiungo cha QR"
356
+ },
357
+ "Auth.SIGNUP_QR_INVALID": {
358
+ "de": "Dieser QR-Link ist ungültig oder fehlt.",
359
+ "fr": "Ce lien QR est invalide ou manquant.",
360
+ "en": "This QR link is invalid or missing.",
361
+ "sw": "Kiungo hiki cha QR si sahihi au hakipo."
362
+ },
363
+ "Auth.SIGNUP_QR_TOKEN_LABEL": {
364
+ "de": "QR-Token",
365
+ "fr": "Jeton QR",
366
+ "en": "QR token",
367
+ "sw": "Tokeni ya QR"
302
368
  },
303
369
  "Auth.EMAIL_LABEL": {
304
370
  "de": "E-Mail-Adresse",
@@ -774,6 +840,12 @@ export const authTranslations = {
774
840
  "en": "Profile updated successfully.",
775
841
  "sw": "Wasifu umesasishwa kwa mafanikio."
776
842
  },
843
+ "Profile.NAME_LABEL": {
844
+ "de": "Name",
845
+ "fr": "Nom",
846
+ "en": "Name",
847
+ "sw": "Jina"
848
+ },
777
849
  "Profile.USERNAME_LABEL": {
778
850
  "de": "Benutzername",
779
851
  "fr": "Nom d’utilisateur",
@@ -13,11 +13,11 @@ const MODE_LABELS = {
13
13
  self_signup_email_domain: 'Auth.SIGNUP_EMAIL_DOMAIN_TAB',
14
14
  self_signup_qr: 'Auth.SIGNUP_QR_TAB',
15
15
  };
16
- const MODE_HINTS = {
17
- self_signup_access_code: 'Auth.SIGNUP_ACCESS_CODE_HINT',
18
- self_signup_open: 'Auth.SIGNUP_OPEN_HINT',
19
- self_signup_email_domain: 'Auth.SIGNUP_EMAIL_DOMAIN_HINT',
20
- self_signup_qr: 'Auth.SIGNUP_QR_HINT',
16
+ const MODE_SUBTITLES = {
17
+ self_signup_access_code: 'Auth.PAGE_SIGNUP_SUBTITLE_ACCESS_CODE',
18
+ self_signup_open: 'Auth.PAGE_SIGNUP_SUBTITLE_OPEN',
19
+ self_signup_email_domain: 'Auth.PAGE_SIGNUP_SUBTITLE_EMAIL_DOMAIN',
20
+ self_signup_qr: 'Auth.PAGE_SIGNUP_SUBTITLE_QR',
21
21
  };
22
22
  export function SignUpPage() {
23
23
  const navigate = useNavigate();
@@ -47,33 +47,15 @@ export function SignUpPage() {
47
47
  const [submitting, setSubmitting] = useState(false);
48
48
  const [successKey, setSuccessKey] = useState(null);
49
49
  const [errorKey, setErrorKey] = useState(null);
50
- const [qrHint, setQrHint] = useState('');
51
- const modeHint = useMemo(() => {
52
- if (mode === 'self_signup_access_code') {
53
- return t(MODE_HINTS[mode], 'Use this option only if you were given an access code for signup.');
50
+ const pageSubtitle = useMemo(() => {
51
+ if (mode === 'self_signup_qr' && tokenFromUrl) {
52
+ return t('Auth.PAGE_SIGNUP_SUBTITLE_QR_READY', 'Enter your email address to complete sign-up with the QR link you opened.');
54
53
  }
55
- if (mode === 'self_signup_open') {
56
- return t(MODE_HINTS[mode], 'Use this option for direct signup without an access code.');
57
- }
58
- if (mode === 'self_signup_email_domain') {
59
- return t(MODE_HINTS[mode], 'Use an email address from an allowed domain for this signup flow.');
60
- }
61
- if (mode === 'self_signup_qr') {
62
- return qrHint || t(MODE_HINTS[mode], 'Use a valid QR signup link to continue.');
63
- }
64
- return '';
65
- }, [mode, qrHint, t]);
54
+ return t(MODE_SUBTITLES[mode] || 'Auth.PAGE_SIGNUP_SUBTITLE', 'Choose a sign-up method and enter the required details.');
55
+ }, [mode, t, tokenFromUrl]);
66
56
  useEffect(() => {
67
57
  setMode(initialMode);
68
58
  }, [initialMode]);
69
- useEffect(() => {
70
- if (!tokenFromUrl || mode !== 'self_signup_qr') {
71
- setQrHint('');
72
- return undefined;
73
- }
74
- setQrHint(t('Auth.SIGNUP_QR_READY', 'QR signup token detected. You can complete your signup now.'));
75
- return undefined;
76
- }, [mode, tokenFromUrl, t]);
77
59
  const handleSubmit = async (event) => {
78
60
  event.preventDefault();
79
61
  setSuccessKey(null);
@@ -110,7 +92,7 @@ export function SignUpPage() {
110
92
  const handleGoToLogin = () => {
111
93
  navigate('/login');
112
94
  };
113
- return (_jsxs(NarrowPage, { title: t('Auth.LOGIN_SIGNUP_BUTTON'), subtitle: t('Auth.PAGE_SIGNUP_SUBTITLE'), 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 && (_jsxs(Stack, { spacing: 1, sx: { mb: 2 }, children: [_jsx(Alert, { severity: "info", children: t('Auth.SIGNUP_MODE_SELECTOR_HINT', 'Choose the signup option that matches how you want to register.') }), _jsx(Stack, { direction: { xs: 'column', sm: 'row' }, spacing: 1, flexWrap: "wrap", children: signupModes.map((entry) => (_jsx(Button, { variant: mode === entry ? 'contained' : 'outlined', onClick: () => setMode(entry), disabled: submitting, children: t(MODE_LABELS[entry] || entry, entry) }, entry))) })] })), _jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { display: 'flex', flexDirection: 'column', gap: 2 }, children: [modeHint && _jsx(Alert, { severity: "info", children: modeHint }), _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
114
96
  ? t('Auth.SIGNUP_SUBMITTING')
115
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') })] }) })] }));
116
98
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "2.2.1",
3
+ "version": "2.2.3",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "private": false,
@@ -88,6 +88,9 @@ export function QrSignupManager({ enabled = false }) {
88
88
  <Typography variant="body2" sx={{ mb: 2, color: 'text.secondary' }}>
89
89
  {t('Auth.SIGNUP_QR_MANAGER_HINT', 'When QR signup is enabled, a signup QR code is shown here immediately.')}
90
90
  </Typography>
91
+ <Typography variant="body2" sx={{ mb: 2, color: 'text.secondary' }}>
92
+ {t('Auth.SIGNUP_QR_POLICY_HINT', 'The default QR validity is configured in the authentication policy above.')}
93
+ </Typography>
91
94
  {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
92
95
  {success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
93
96
 
@@ -121,6 +124,11 @@ export function QrSignupManager({ enabled = false }) {
121
124
  sx={{ mt: 2 }}
122
125
  InputProps={{ readOnly: true }}
123
126
  />
127
+ {result?.expires_at && (
128
+ <Typography variant="body2" sx={{ mt: 1.5, color: 'text.secondary' }}>
129
+ {t('Auth.SIGNUP_QR_VALID_UNTIL', 'Valid until')}: {result.expires_at}
130
+ </Typography>
131
+ )}
124
132
  </Box>
125
133
  )}
126
134
  </Box>
@@ -20,12 +20,14 @@ const EMPTY_POLICY = {
20
20
  allow_self_signup_qr: false,
21
21
  allowed_email_domains: [],
22
22
  required_auth_factor_count: 1,
23
+ signup_qr_expiry_days: 90,
23
24
  };
24
25
 
25
26
  export function RegistrationMethodsManager({ onPolicyChange }) {
26
27
  const { t } = useTranslation();
27
28
  const [policy, setPolicy] = useState(EMPTY_POLICY);
28
29
  const [domainsText, setDomainsText] = useState('');
30
+ const [signupQrExpiryDays, setSignupQrExpiryDays] = useState(String(EMPTY_POLICY.signup_qr_expiry_days));
29
31
  const [busy, setBusy] = useState(false);
30
32
  const [busyField, setBusyField] = useState('');
31
33
  const [error, setError] = useState('');
@@ -39,6 +41,7 @@ export function RegistrationMethodsManager({ onPolicyChange }) {
39
41
  if (!active) return;
40
42
  setPolicy((prev) => ({ ...prev, ...data }));
41
43
  setDomainsText((data?.allowed_email_domains || []).join('\n'));
44
+ setSignupQrExpiryDays(String(data?.signup_qr_expiry_days || EMPTY_POLICY.signup_qr_expiry_days));
42
45
  if (onPolicyChange) onPolicyChange(data);
43
46
  } catch (err) {
44
47
  if (active) {
@@ -80,9 +83,16 @@ export function RegistrationMethodsManager({ onPolicyChange }) {
80
83
  .split(/\r?\n/)
81
84
  .map((value) => value.trim())
82
85
  .filter(Boolean);
83
- const next = await updateAuthPolicy({ allowed_email_domains });
86
+ const parsedExpiryDays = parseInt(signupQrExpiryDays, 10);
87
+ const next = await updateAuthPolicy({
88
+ allowed_email_domains,
89
+ signup_qr_expiry_days: Number.isFinite(parsedExpiryDays) && parsedExpiryDays > 0
90
+ ? parsedExpiryDays
91
+ : EMPTY_POLICY.signup_qr_expiry_days,
92
+ });
84
93
  setPolicy((prev) => ({ ...prev, ...next }));
85
94
  setDomainsText((next?.allowed_email_domains || allowed_email_domains).join('\n'));
95
+ setSignupQrExpiryDays(String(next?.signup_qr_expiry_days || EMPTY_POLICY.signup_qr_expiry_days));
86
96
  setSuccess(t('Auth.AUTH_POLICY_SAVE_SUCCESS', 'Authentication settings saved.'));
87
97
  if (onPolicyChange) onPolicyChange(next);
88
98
  } catch (err) {
@@ -176,6 +186,19 @@ export function RegistrationMethodsManager({ onPolicyChange }) {
176
186
  onChange={(event) => setDomainsText(event.target.value)}
177
187
  />
178
188
 
189
+ <TextField
190
+ label={t('Auth.SIGNUP_QR_EXPIRY_DAYS_LABEL', 'QR signup validity (days)')}
191
+ helperText={t(
192
+ 'Auth.SIGNUP_QR_EXPIRY_DAYS_HINT',
193
+ 'Default validity for newly generated QR signup links.',
194
+ )}
195
+ type="number"
196
+ fullWidth
197
+ sx={{ mt: 2 }}
198
+ value={signupQrExpiryDays}
199
+ onChange={(event) => setSignupQrExpiryDays(event.target.value)}
200
+ />
201
+
179
202
  <Button variant="contained" sx={{ mt: 2 }} onClick={save} disabled={busy}>
180
203
  {t('Common.SAVE', 'Save')}
181
204
  </Button>
@@ -333,10 +333,76 @@ export const authTranslations = {
333
333
  "sw": "Tafadhali ingiza anwani ya barua pepe."
334
334
  },
335
335
  "Auth.PAGE_SIGNUP_SUBTITLE": {
336
- "de": "Geben Sie Ihre E-Mail-Adresse und den Zugangscode ein, um eine Einladung anzufordern.",
337
- "fr": "Saisissez votre adresse e-mail et le code d'accès pour demander une invitation.",
338
- "en": "Enter your email address and access code to request an invitation.",
339
- "sw": "Ingiza barua pepe yako na msimbo wa ufikiaji ili kuomba mwaliko."
336
+ "de": "Wählen Sie eine Registrierungsart und geben Sie die benötigten Angaben ein.",
337
+ "fr": "Choisissez une méthode d'inscription et saisissez les informations requises.",
338
+ "en": "Choose a sign-up method and enter the required details.",
339
+ "sw": "Chagua njia ya kujisajili na uingize taarifa zinazohitajika."
340
+ },
341
+ "Auth.PAGE_SIGNUP_SUBTITLE_ACCESS_CODE": {
342
+ "de": "Geben Sie Ihre E-Mail-Adresse und den erhaltenen Zugangscode ein.",
343
+ "fr": "Saisissez votre adresse e-mail et le code d'accès reçu.",
344
+ "en": "Enter your email address and the access code you received.",
345
+ "sw": "Ingiza anwani yako ya barua pepe na msimbo wa ufikiaji uliopokea."
346
+ },
347
+ "Auth.PAGE_SIGNUP_SUBTITLE_OPEN": {
348
+ "de": "Geben Sie Ihre E-Mail-Adresse ein, um sich direkt zu registrieren.",
349
+ "fr": "Saisissez votre adresse e-mail pour vous inscrire directement.",
350
+ "en": "Enter your email address to sign up directly.",
351
+ "sw": "Ingiza anwani yako ya barua pepe ili kujisajili moja kwa moja."
352
+ },
353
+ "Auth.PAGE_SIGNUP_SUBTITLE_EMAIL_DOMAIN": {
354
+ "de": "Verwenden Sie eine E-Mail-Adresse aus einer freigegebenen Domain.",
355
+ "fr": "Utilisez une adresse e-mail provenant d'un domaine autorisé.",
356
+ "en": "Use an email address from an approved domain.",
357
+ "sw": "Tumia anwani ya barua pepe kutoka kwenye domeni iliyoidhinishwa."
358
+ },
359
+ "Auth.PAGE_SIGNUP_SUBTITLE_QR": {
360
+ "de": "Öffnen Sie diese Seite über einen gültigen QR-Link und geben Sie dann Ihre E-Mail-Adresse ein.",
361
+ "fr": "Ouvrez cette page via un lien QR valide, puis saisissez votre adresse e-mail.",
362
+ "en": "Open this page from a valid QR link, then enter your email address.",
363
+ "sw": "Fungua ukurasa huu kupitia kiungo halali cha QR, kisha ingiza anwani yako ya barua pepe."
364
+ },
365
+ "Auth.PAGE_SIGNUP_SUBTITLE_QR_READY": {
366
+ "de": "Geben Sie Ihre E-Mail-Adresse ein, um die Registrierung mit diesem QR-Link abzuschließen.",
367
+ "fr": "Saisissez votre adresse e-mail pour terminer l'inscription avec ce lien QR.",
368
+ "en": "Enter your email address to complete sign-up with this QR link.",
369
+ "sw": "Ingiza anwani yako ya barua pepe ili kukamilisha kujisajili kwa kutumia kiungo hiki cha QR."
370
+ },
371
+ "Auth.SIGNUP_ACCESS_CODE_TAB": {
372
+ "de": "Mit Zugangscode",
373
+ "fr": "Avec code d'accès",
374
+ "en": "With access code",
375
+ "sw": "Kwa msimbo wa ufikiaji"
376
+ },
377
+ "Auth.SIGNUP_OPEN_TAB": {
378
+ "de": "Direkte Registrierung",
379
+ "fr": "Inscription directe",
380
+ "en": "Direct sign-up",
381
+ "sw": "Kujisajili moja kwa moja"
382
+ },
383
+ "Auth.SIGNUP_EMAIL_DOMAIN_TAB": {
384
+ "de": "Mit E-Mail-Domain",
385
+ "fr": "Avec domaine e-mail",
386
+ "en": "With email domain",
387
+ "sw": "Kwa domeni ya barua pepe"
388
+ },
389
+ "Auth.SIGNUP_QR_TAB": {
390
+ "de": "Mit QR-Link",
391
+ "fr": "Avec lien QR",
392
+ "en": "With QR link",
393
+ "sw": "Kwa kiungo cha QR"
394
+ },
395
+ "Auth.SIGNUP_QR_INVALID": {
396
+ "de": "Dieser QR-Link ist ungültig oder fehlt.",
397
+ "fr": "Ce lien QR est invalide ou manquant.",
398
+ "en": "This QR link is invalid or missing.",
399
+ "sw": "Kiungo hiki cha QR si sahihi au hakipo."
400
+ },
401
+ "Auth.SIGNUP_QR_TOKEN_LABEL": {
402
+ "de": "QR-Token",
403
+ "fr": "Jeton QR",
404
+ "en": "QR token",
405
+ "sw": "Tokeni ya QR"
340
406
  },
341
407
 
342
408
  "Auth.EMAIL_LABEL": {
@@ -819,6 +885,12 @@ export const authTranslations = {
819
885
  "en": "Profile updated successfully.",
820
886
  "sw": "Wasifu umesasishwa kwa mafanikio."
821
887
  },
888
+ "Profile.NAME_LABEL": {
889
+ "de": "Name",
890
+ "fr": "Nom",
891
+ "en": "Name",
892
+ "sw": "Jina"
893
+ },
822
894
  "Profile.USERNAME_LABEL": {
823
895
  "de": "Benutzername",
824
896
  "fr": "Nom d’utilisateur",
@@ -21,11 +21,11 @@ const MODE_LABELS = {
21
21
  self_signup_qr: 'Auth.SIGNUP_QR_TAB',
22
22
  };
23
23
 
24
- const MODE_HINTS = {
25
- self_signup_access_code: 'Auth.SIGNUP_ACCESS_CODE_HINT',
26
- self_signup_open: 'Auth.SIGNUP_OPEN_HINT',
27
- self_signup_email_domain: 'Auth.SIGNUP_EMAIL_DOMAIN_HINT',
28
- self_signup_qr: 'Auth.SIGNUP_QR_HINT',
24
+ const MODE_SUBTITLES = {
25
+ self_signup_access_code: 'Auth.PAGE_SIGNUP_SUBTITLE_ACCESS_CODE',
26
+ self_signup_open: 'Auth.PAGE_SIGNUP_SUBTITLE_OPEN',
27
+ self_signup_email_domain: 'Auth.PAGE_SIGNUP_SUBTITLE_EMAIL_DOMAIN',
28
+ self_signup_qr: 'Auth.PAGE_SIGNUP_SUBTITLE_QR',
29
29
  };
30
30
 
31
31
  export function SignUpPage() {
@@ -60,46 +60,24 @@ export function SignUpPage() {
60
60
  const [submitting, setSubmitting] = useState(false);
61
61
  const [successKey, setSuccessKey] = useState(null);
62
62
  const [errorKey, setErrorKey] = useState(null);
63
- const [qrHint, setQrHint] = useState('');
64
63
 
65
- const modeHint = useMemo(() => {
66
- if (mode === 'self_signup_access_code') {
64
+ const pageSubtitle = useMemo(() => {
65
+ if (mode === 'self_signup_qr' && tokenFromUrl) {
67
66
  return t(
68
- MODE_HINTS[mode],
69
- 'Use this option only if you were given an access code for signup.',
67
+ 'Auth.PAGE_SIGNUP_SUBTITLE_QR_READY',
68
+ 'Enter your email address to complete sign-up with the QR link you opened.',
70
69
  );
71
70
  }
72
- if (mode === 'self_signup_open') {
73
- return t(
74
- MODE_HINTS[mode],
75
- 'Use this option for direct signup without an access code.',
76
- );
77
- }
78
- if (mode === 'self_signup_email_domain') {
79
- return t(
80
- MODE_HINTS[mode],
81
- 'Use an email address from an allowed domain for this signup flow.',
82
- );
83
- }
84
- if (mode === 'self_signup_qr') {
85
- return qrHint || t(MODE_HINTS[mode], 'Use a valid QR signup link to continue.');
86
- }
87
- return '';
88
- }, [mode, qrHint, t]);
71
+ return t(
72
+ MODE_SUBTITLES[mode] || 'Auth.PAGE_SIGNUP_SUBTITLE',
73
+ 'Choose a sign-up method and enter the required details.',
74
+ );
75
+ }, [mode, t, tokenFromUrl]);
89
76
 
90
77
  useEffect(() => {
91
78
  setMode(initialMode);
92
79
  }, [initialMode]);
93
80
 
94
- useEffect(() => {
95
- if (!tokenFromUrl || mode !== 'self_signup_qr') {
96
- setQrHint('');
97
- return undefined;
98
- }
99
- setQrHint(t('Auth.SIGNUP_QR_READY', 'QR signup token detected. You can complete your signup now.'));
100
- return undefined;
101
- }, [mode, tokenFromUrl, t]);
102
-
103
81
  const handleSubmit = async (event) => {
104
82
  event.preventDefault();
105
83
  setSuccessKey(null);
@@ -143,7 +121,7 @@ export function SignUpPage() {
143
121
  return (
144
122
  <NarrowPage
145
123
  title={t('Auth.LOGIN_SIGNUP_BUTTON')}
146
- subtitle={t('Auth.PAGE_SIGNUP_SUBTITLE')}
124
+ subtitle={pageSubtitle}
147
125
  >
148
126
  <Helmet>
149
127
  <title>
@@ -165,19 +143,14 @@ export function SignUpPage() {
165
143
 
166
144
  {signupModes.length > 1 && (
167
145
  <Stack spacing={1} sx={{ mb: 2 }}>
168
- <Alert severity="info">
169
- {t(
170
- 'Auth.SIGNUP_MODE_SELECTOR_HINT',
171
- 'Choose the signup option that matches how you want to register.',
172
- )}
173
- </Alert>
174
- <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1} flexWrap="wrap">
146
+ <Stack spacing={1}>
175
147
  {signupModes.map((entry) => (
176
148
  <Button
177
149
  key={entry}
178
150
  variant={mode === entry ? 'contained' : 'outlined'}
179
151
  onClick={() => setMode(entry)}
180
152
  disabled={submitting}
153
+ fullWidth
181
154
  >
182
155
  {t(MODE_LABELS[entry] || entry, entry)}
183
156
  </Button>
@@ -191,8 +164,6 @@ export function SignUpPage() {
191
164
  onSubmit={handleSubmit}
192
165
  sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}
193
166
  >
194
- {modeHint && <Alert severity="info">{modeHint}</Alert>}
195
-
196
167
  <TextField
197
168
  label={t('Auth.EMAIL_LABEL')}
198
169
  type="email"
@@ -215,17 +186,6 @@ export function SignUpPage() {
215
186
  />
216
187
  )}
217
188
 
218
- {mode === 'self_signup_qr' && (
219
- <Stack spacing={1}>
220
- <TextField
221
- label={t('Auth.SIGNUP_QR_TOKEN_LABEL', 'QR token')}
222
- value={tokenFromUrl}
223
- fullWidth
224
- InputProps={{ readOnly: true }}
225
- />
226
- </Stack>
227
- )}
228
-
229
189
  <Button
230
190
  type="submit"
231
191
  variant="contained"