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

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.
@@ -19,15 +19,6 @@ export async function fetchAuthMethods() {
19
19
  const res = await apiClient.get('/api/auth-methods/');
20
20
  return res.data || {};
21
21
  }
22
- export async function fetchRegistrationOptions() {
23
- try {
24
- const res = await apiClient.get(`${USERS_BASE}/registration-options/`);
25
- return res.data || {};
26
- }
27
- catch (error) {
28
- throw normaliseApiError(error, 'Auth.REGISTRATION_OPTIONS_FAILED');
29
- }
30
- }
31
22
  export async function fetchAuthPolicy() {
32
23
  try {
33
24
  const res = await apiClient.get(`${USERS_BASE}/auth-policy/`);
@@ -372,16 +363,6 @@ export async function createSignupQr(payload = {}) {
372
363
  throw normaliseApiError(error, 'Auth.SIGNUP_QR_CREATE_FAILED');
373
364
  }
374
365
  }
375
- export async function requestInviteWithCode(email, accessCode) {
376
- if (accessCode) {
377
- return submitRegistrationRequest({
378
- email,
379
- mode: 'self_signup_access_code',
380
- accessCode,
381
- });
382
- }
383
- return sendAdminInvite(email);
384
- }
385
366
  // -----------------------------
386
367
  // Recovery Support (Admin/Support Side)
387
368
  // -----------------------------
@@ -1,6 +1,6 @@
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, FormControl, FormControlLabel, Radio, RadioGroup, Typography, } from '@mui/material';
3
+ import { Alert, Box, FormControl, FormControlLabel, Radio, RadioGroup, Typography, } from '@mui/material';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { fetchAuthPolicy, updateAuthPolicy } from '../auth/authApi';
6
6
  export function AuthFactorRequirementCard() {
@@ -26,20 +26,24 @@ export function AuthFactorRequirementCard() {
26
26
  active = false;
27
27
  };
28
28
  }, []);
29
- const save = async () => {
29
+ const handleChange = async (event) => {
30
+ const nextValue = event.target.value;
31
+ const previous = value;
32
+ setValue(nextValue);
30
33
  setBusy(true);
31
34
  setError('');
32
35
  setSuccess('');
33
36
  try {
34
- await updateAuthPolicy({ required_auth_factor_count: Number(value) });
37
+ await updateAuthPolicy({ required_auth_factor_count: Number(nextValue) });
35
38
  setSuccess(t('Auth.AUTH_FACTOR_SAVE_SUCCESS', 'Factor requirement saved.'));
36
39
  }
37
40
  catch (err) {
41
+ setValue(previous);
38
42
  setError(t((err === null || err === void 0 ? void 0 : err.code) || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save factor requirement.'));
39
43
  }
40
44
  finally {
41
45
  setBusy(false);
42
46
  }
43
47
  };
44
- return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.AUTH_FACTOR_TITLE', 'Authentication Factors') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Auth.AUTH_FACTOR_HINT', 'Choose whether one factor is enough or two factors are required.') }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error }), success && _jsx(Alert, { severity: "success", sx: { mb: 2 }, children: success }), _jsx(FormControl, { children: _jsxs(RadioGroup, { value: value, onChange: (event) => setValue(event.target.value), children: [_jsx(FormControlLabel, { value: "1", control: _jsx(Radio, {}), label: t('Auth.ONE_FACTOR_LABEL', 'One factor is enough') }), _jsx(FormControlLabel, { value: "2", control: _jsx(Radio, {}), label: t('Auth.TWO_FACTOR_LABEL', 'Two factors are required') })] }) }), _jsx(Button, { variant: "contained", sx: { mt: 2 }, onClick: save, disabled: busy, children: t('Common.SAVE', 'Save') })] }));
48
+ return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.AUTH_FACTOR_TITLE', 'Authentication Factors') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Auth.AUTH_FACTOR_HINT', 'Choose whether one factor is enough or two factors are required.') }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error }), success && _jsx(Alert, { severity: "success", sx: { mb: 2 }, children: success }), _jsx(FormControl, { children: _jsxs(RadioGroup, { value: value, onChange: handleChange, children: [_jsx(FormControlLabel, { value: "1", control: _jsx(Radio, { disabled: busy }), label: t('Auth.ONE_FACTOR_LABEL', 'One factor is enough') }), _jsx(FormControlLabel, { value: "2", control: _jsx(Radio, { disabled: busy }), label: t('Auth.TWO_FACTOR_LABEL', 'Two factors are required') })] }) })] }));
45
49
  }
@@ -21,6 +21,6 @@ disabled = false, initialIdentifier = '', }) {
21
21
  const supportsPasskey = !!onPasskeyLogin &&
22
22
  typeof window !== 'undefined' &&
23
23
  !!window.PublicKeyCredential;
24
- return (_jsxs(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 3 }, children: [error && (_jsx(Typography, { color: "error", gutterBottom: true, children: error })), supportsPasskey && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "contained", fullWidth: true, type: "button", onClick: onPasskeyLogin, disabled: disabled, children: t('Auth.LOGIN_USE_PASSKEY_BUTTON') }), _jsx(Divider, { sx: { my: 2 }, children: t('Auth.LOGIN_OR') })] })), onSubmit && (_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: identifier, onChange: (e) => setIdentifier(e.target.value), disabled: disabled }), _jsx(TextField, { label: t('Auth.LOGIN_PASSWORD_LABEL'), type: "password", required: true, fullWidth: true, value: password, onChange: (e) => setPassword(e.target.value), disabled: disabled }), _jsx(Button, { type: "submit", variant: "contained", fullWidth: true, disabled: disabled, children: t('Auth.PAGE_LOGIN_TITLE') })] })), onSocialLogin && (_jsxs(Box, { children: [_jsx(Divider, { sx: { my: 2 }, children: t('Auth.LOGIN_OR') }), _jsx(SocialLoginButtons, { onProviderClick: onSocialLogin, providers: socialProviders })] })), (onSignUp || onForgotPassword) && (_jsxs(Box, { children: [_jsx(Typography, { variant: "subtitle2", sx: { mb: 1 }, children: t('Auth.LOGIN_ACCOUNT_RECOVERY_TITLE') }), _jsxs(Box, { sx: { display: 'flex', gap: 1, flexWrap: 'wrap' }, children: [onSignUp && (_jsx(Button, { type: "button", variant: "outlined", onClick: onSignUp, disabled: disabled, children: t('Auth.LOGIN_SIGNUP_BUTTON') })), onForgotPassword && (_jsx(Button, { type: "button", variant: "outlined", onClick: onForgotPassword, disabled: disabled, children: t('Auth.LOGIN_FORGOT_PASSWORD_BUTTON') }))] })] }))] }));
24
+ return (_jsxs(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 3 }, children: [error && (_jsx(Typography, { color: "error", gutterBottom: true, children: error })), supportsPasskey && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "contained", fullWidth: true, type: "button", onClick: onPasskeyLogin, disabled: disabled, children: t('Auth.LOGIN_USE_PASSKEY_BUTTON') }), _jsx(Divider, { sx: { my: 2 }, children: t('Auth.LOGIN_OR') })] })), onSubmit && (_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: identifier, onChange: (e) => setIdentifier(e.target.value), disabled: disabled }), _jsx(TextField, { label: t('Auth.LOGIN_PASSWORD_LABEL'), type: "password", required: true, fullWidth: true, value: password, onChange: (e) => setPassword(e.target.value), disabled: disabled }), _jsx(Button, { type: "submit", variant: "contained", fullWidth: true, disabled: disabled, children: t('Auth.PAGE_LOGIN_TITLE') })] })), onSocialLogin && (_jsxs(Box, { children: [_jsx(Divider, { sx: { my: 2 }, children: t('Auth.LOGIN_OR') }), _jsx(SocialLoginButtons, { onProviderClick: onSocialLogin, providers: socialProviders })] })), (onSignUp || onForgotPassword) && (_jsxs(Box, { children: [_jsx(Divider, { sx: { my: 2 }, children: t('Auth.LOGIN_OR') }), _jsxs(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 1.5 }, children: [onSignUp && (_jsx(Button, { type: "button", variant: "outlined", onClick: onSignUp, disabled: disabled, fullWidth: true, children: t('Auth.LOGIN_SIGNUP_BUTTON') })), onForgotPassword && (_jsx(Button, { type: "button", variant: "outlined", onClick: onForgotPassword, disabled: disabled, fullWidth: true, children: t('Auth.LOGIN_FORGOT_PASSWORD_BUTTON') }))] })] }))] }));
25
25
  }
26
26
  ;
@@ -1,36 +1,32 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React, { useState } from 'react';
3
- import { Alert, Box, Button, Stack, TextField, Typography, } from '@mui/material';
2
+ import React, { useEffect, useRef, useState } from 'react';
3
+ import { Alert, Box, Button, TextField, 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() {
7
+ export function QrSignupManager({ enabled = false }) {
8
8
  const { t } = useTranslation();
9
9
  const [label, setLabel] = useState('');
10
- const [eventRef, setEventRef] = useState('');
11
- const [courseRef, setCourseRef] = useState('');
12
- const [groupRef, setGroupRef] = useState('');
13
10
  const [busy, setBusy] = useState(false);
14
11
  const [error, setError] = useState('');
12
+ const [success, setSuccess] = useState('');
15
13
  const [result, setResult] = useState(null);
14
+ const hasGeneratedRef = useRef(false);
16
15
  const generate = async () => {
16
+ if (!enabled) {
17
+ setResult(null);
18
+ return;
19
+ }
17
20
  setBusy(true);
18
21
  setError('');
22
+ setSuccess('');
19
23
  try {
20
- const registrationContext = {
21
- schema_version: '1',
22
- };
23
- if (eventRef.trim())
24
- registrationContext.event_ref = eventRef.trim();
25
- if (courseRef.trim())
26
- registrationContext.course_ref = courseRef.trim();
27
- if (groupRef.trim())
28
- registrationContext.group_ref = groupRef.trim();
29
24
  const data = await createSignupQr({
30
25
  label,
31
- registration_context: registrationContext,
32
26
  });
33
27
  setResult(data);
28
+ hasGeneratedRef.current = true;
29
+ setSuccess(t('Auth.SIGNUP_QR_SAVE_SUCCESS', 'QR signup link updated.'));
34
30
  }
35
31
  catch (err) {
36
32
  setError(t((err === null || err === void 0 ? void 0 : err.code) || 'Auth.SIGNUP_QR_CREATE_FAILED', 'Could not create signup QR.'));
@@ -39,5 +35,47 @@ export function QrSignupManager() {
39
35
  setBusy(false);
40
36
  }
41
37
  };
42
- 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 a QR code for self-signup and optionally prefill registration context references.') }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error }), _jsxs(Stack, { spacing: 2, children: [_jsx(TextField, { label: t('Common.LABEL', 'Label'), value: label, onChange: (event) => setLabel(event.target.value) }), _jsx(TextField, { label: t('Auth.EVENT_REF_LABEL', 'Event reference'), value: eventRef, onChange: (event) => setEventRef(event.target.value) }), _jsx(TextField, { label: t('Auth.COURSE_REF_LABEL', 'Course reference'), value: courseRef, onChange: (event) => setCourseRef(event.target.value) }), _jsx(TextField, { label: t('Auth.GROUP_REF_LABEL', 'Group reference'), value: groupRef, onChange: (event) => setGroupRef(event.target.value) })] }), _jsx(Button, { variant: "contained", sx: { mt: 2 }, onClick: generate, disabled: busy, children: t('Auth.SIGNUP_QR_CREATE_BUTTON', 'Generate QR') }), (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 } })] }))] }));
38
+ useEffect(() => {
39
+ if (!enabled) {
40
+ setResult(null);
41
+ setError('');
42
+ setSuccess('');
43
+ hasGeneratedRef.current = false;
44
+ return;
45
+ }
46
+ if (hasGeneratedRef.current) {
47
+ return;
48
+ }
49
+ let active = true;
50
+ const ensureInitialQr = async () => {
51
+ setBusy(true);
52
+ setError('');
53
+ setSuccess('');
54
+ try {
55
+ const data = await createSignupQr({
56
+ label,
57
+ });
58
+ if (!active)
59
+ return;
60
+ setResult(data);
61
+ hasGeneratedRef.current = true;
62
+ setSuccess(t('Auth.SIGNUP_QR_SAVE_SUCCESS', 'QR signup link updated.'));
63
+ }
64
+ catch (err) {
65
+ if (!active)
66
+ return;
67
+ setError(t((err === null || err === void 0 ? void 0 : err.code) || 'Auth.SIGNUP_QR_CREATE_FAILED', 'Could not create signup QR.'));
68
+ }
69
+ finally {
70
+ if (active) {
71
+ setBusy(false);
72
+ }
73
+ }
74
+ };
75
+ ensureInitialQr();
76
+ return () => {
77
+ active = false;
78
+ };
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 } })] }))] }));
43
81
  }
@@ -17,6 +17,7 @@ export function RegistrationMethodsManager({ onPolicyChange }) {
17
17
  const [policy, setPolicy] = useState(EMPTY_POLICY);
18
18
  const [domainsText, setDomainsText] = useState('');
19
19
  const [busy, setBusy] = useState(false);
20
+ const [busyField, setBusyField] = useState('');
20
21
  const [error, setError] = useState('');
21
22
  const [success, setSuccess] = useState('');
22
23
  useEffect(() => {
@@ -28,6 +29,8 @@ export function RegistrationMethodsManager({ onPolicyChange }) {
28
29
  return;
29
30
  setPolicy((prev) => (Object.assign(Object.assign({}, prev), data)));
30
31
  setDomainsText(((data === null || data === void 0 ? void 0 : data.allowed_email_domains) || []).join('\n'));
32
+ if (onPolicyChange)
33
+ onPolicyChange(data);
31
34
  }
32
35
  catch (err) {
33
36
  if (active) {
@@ -38,9 +41,28 @@ export function RegistrationMethodsManager({ onPolicyChange }) {
38
41
  return () => {
39
42
  active = false;
40
43
  };
41
- }, [t]);
42
- const toggle = (field) => (_event, checked) => {
44
+ }, [onPolicyChange, t]);
45
+ const toggle = (field) => async (_event, checked) => {
46
+ const previous = policy[field];
43
47
  setPolicy((prev) => (Object.assign(Object.assign({}, prev), { [field]: checked })));
48
+ setBusyField(field);
49
+ setError('');
50
+ setSuccess('');
51
+ try {
52
+ const next = await updateAuthPolicy({ [field]: checked });
53
+ setPolicy((prev) => (Object.assign(Object.assign({}, prev), next)));
54
+ setDomainsText(((next === null || next === void 0 ? void 0 : next.allowed_email_domains) || []).join('\n'));
55
+ setSuccess(t('Auth.AUTH_POLICY_SAVE_SUCCESS', 'Authentication settings saved.'));
56
+ if (onPolicyChange)
57
+ onPolicyChange(next);
58
+ }
59
+ catch (err) {
60
+ setPolicy((prev) => (Object.assign(Object.assign({}, prev), { [field]: previous })));
61
+ setError(t((err === null || err === void 0 ? void 0 : err.code) || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save authentication settings.'));
62
+ }
63
+ finally {
64
+ setBusyField('');
65
+ }
44
66
  };
45
67
  const save = async () => {
46
68
  setBusy(true);
@@ -51,7 +73,7 @@ export function RegistrationMethodsManager({ onPolicyChange }) {
51
73
  .split(/\r?\n/)
52
74
  .map((value) => value.trim())
53
75
  .filter(Boolean);
54
- const next = await updateAuthPolicy(Object.assign(Object.assign({}, policy), { allowed_email_domains }));
76
+ const next = await updateAuthPolicy({ allowed_email_domains });
55
77
  setPolicy((prev) => (Object.assign(Object.assign({}, prev), next)));
56
78
  setDomainsText(((next === null || next === void 0 ? void 0 : next.allowed_email_domains) || allowed_email_domains).join('\n'));
57
79
  setSuccess(t('Auth.AUTH_POLICY_SAVE_SUCCESS', 'Authentication settings saved.'));
@@ -65,5 +87,5 @@ export function RegistrationMethodsManager({ onPolicyChange }) {
65
87
  setBusy(false);
66
88
  }
67
89
  };
68
- 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') }), 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') }), 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') }), 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') }), 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') }), label: t('Auth.SIGNUP_QR_LABEL', 'Self-signup by QR') })] }), _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'), 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') })] }));
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') })] }));
69
91
  }
@@ -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",
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React, { useContext, useMemo } from 'react';
2
+ import React, { useContext, useMemo, useState } from 'react';
3
3
  import { Helmet } from 'react-helmet';
4
4
  import { useSearchParams } from 'react-router-dom';
5
5
  import { Tabs, Tab, Box, Typography, Alert, CircularProgress, Paper, Stack, } from '@mui/material';
@@ -24,6 +24,7 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
24
24
  const { t } = useTranslation();
25
25
  const { user, login, loading } = useContext(AuthContext);
26
26
  const [searchParams, setSearchParams] = useSearchParams();
27
+ const [authPolicy, setAuthPolicy] = useState(null);
27
28
  // 1. URL State Management
28
29
  const currentTabRaw = searchParams.get('tab') || 'profile';
29
30
  const currentTab = ['invite', 'bulk-invite-csv', 'access'].includes(currentTabRaw)
@@ -90,5 +91,5 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
90
91
  const activeExtraTab = builtInTabValues.has(safeTab)
91
92
  ? null
92
93
  : extraTabs.find((tab) => tab.value === safeTab);
93
- return (_jsxs(WidePage, { title: t('Account.TITLE', 'Account & Administration'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('Account.PAGE_TITLE', 'Account'), " \u2013 ", user.email] }) }), _jsx(Tabs, { value: safeTab, onChange: handleTabChange, variant: "scrollable", scrollButtons: "auto", sx: { mb: 3, borderBottom: 1, borderColor: 'divider' }, children: tabs.map((tab) => (_jsx(Tab, { label: tab.label, value: tab.value }, tab.value))) }), safeTab === 'profile' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(ProfileComponent, { onSubmit: handleProfileSubmit, showName: true, showPrivacy: true, showCookies: true }) })), safeTab === 'security' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SecurityComponent, { fromRecovery: fromRecovery, fromWeakLogin: fromWeakLogin }) })), safeTab === 'users' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(UserListComponent, { roles: activeRoles, currentUser: user, extraColumns: userListExtraColumns, extraRowActions: userListExtraRowActions, extraContext: userListExtraContext, refreshTrigger: userListRefreshTrigger, canEditUser: userListCanEditUser }) })), safeTab === 'invite' && (_jsx(Box, { sx: { mt: 2 }, children: _jsxs(Stack, { spacing: 2.5, children: [(isSuperUser || perms.can_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(RegistrationMethodsManager, {}) })), (isSuperUser || perms.can_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(AuthFactorRequirementCard, {}) })), (isSuperUser || perms.can_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(UserInviteComponent, {}) })), (isSuperUser || perms.can_manage_access_codes) && (_jsxs(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: [_jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Account.ACCESS_CODES_HINT', 'Manage access codes for self-registration.') }), _jsx(AccessCodeManager, {})] })), (isSuperUser || perms.can_invite) && showBulkInviteCsvTab && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(BulkInviteCsvTab, Object.assign({}, bulkInviteCsvProps)) })), (isSuperUser || perms.can_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(QrSignupManager, {}) }))] }) })), safeTab === 'support' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SupportRecoveryRequestsTab, {}) })), activeExtraTab && (_jsx(Box, { sx: { mt: 2 }, children: (_a = activeExtraTab.render) === null || _a === void 0 ? void 0 : _a.call(activeExtraTab, { user, perms, isSuperUser, t }) }))] }));
94
+ return (_jsxs(WidePage, { title: t('Account.TITLE', 'Account & Administration'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('Account.PAGE_TITLE', 'Account'), " \u2013 ", user.email] }) }), _jsx(Tabs, { value: safeTab, onChange: handleTabChange, variant: "scrollable", scrollButtons: "auto", sx: { mb: 3, borderBottom: 1, borderColor: 'divider' }, children: tabs.map((tab) => (_jsx(Tab, { label: tab.label, value: tab.value }, tab.value))) }), safeTab === 'profile' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(ProfileComponent, { onSubmit: handleProfileSubmit, showName: true, showPrivacy: true, showCookies: true }) })), safeTab === 'security' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SecurityComponent, { fromRecovery: fromRecovery, fromWeakLogin: fromWeakLogin }) })), safeTab === 'users' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(UserListComponent, { roles: activeRoles, currentUser: user, extraColumns: userListExtraColumns, extraRowActions: userListExtraRowActions, extraContext: userListExtraContext, refreshTrigger: userListRefreshTrigger, canEditUser: userListCanEditUser }) })), safeTab === 'invite' && (_jsx(Box, { sx: { mt: 2 }, children: _jsxs(Stack, { spacing: 2.5, children: [(isSuperUser || perms.can_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(RegistrationMethodsManager, { onPolicyChange: setAuthPolicy }) })), (isSuperUser || perms.can_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(AuthFactorRequirementCard, {}) })), (isSuperUser || perms.can_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(UserInviteComponent, {}) })), (isSuperUser || perms.can_manage_access_codes) && (_jsxs(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: [_jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Account.ACCESS_CODES_HINT', 'Manage access codes for self-registration.') }), _jsx(AccessCodeManager, {})] })), (isSuperUser || perms.can_invite) && showBulkInviteCsvTab && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(BulkInviteCsvTab, Object.assign({}, bulkInviteCsvProps)) })), (isSuperUser || perms.can_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(QrSignupManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) }) }))] }) })), safeTab === 'support' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SupportRecoveryRequestsTab, {}) })), activeExtraTab && (_jsx(Box, { sx: { mt: 2 }, children: (_a = activeExtraTab.render) === null || _a === void 0 ? void 0 : _a.call(activeExtraTab, { user, perms, isSuperUser, t }) }))] }));
94
95
  }
@@ -32,6 +32,21 @@ export function LoginPage() {
32
32
  : recoveryTokenRaw;
33
33
  // Backward-compatible fallback for legacy links using query parameters.
34
34
  const recoveryEmail = hashParams.get('email') || params.get('email') || '';
35
+ const requestedNext = params.get('next');
36
+ const getRedirectTarget = (currentUser, options = {}) => {
37
+ var _a;
38
+ if (options.forceSecurityRedirect) {
39
+ return options.forceSecurityRedirect;
40
+ }
41
+ const requiresExtra = ((_a = currentUser === null || currentUser === void 0 ? void 0 : currentUser.security_state) === null || _a === void 0 ? void 0 : _a.requires_additional_security) === true;
42
+ if (requiresExtra) {
43
+ return '/account?tab=security&from=weak_login';
44
+ }
45
+ if (requestedNext && requestedNext.startsWith('/')) {
46
+ return requestedNext;
47
+ }
48
+ return '/';
49
+ };
35
50
  useEffect(() => {
36
51
  const socialError = params.get('error') || params.get('social');
37
52
  if (socialError) {
@@ -39,30 +54,14 @@ export function LoginPage() {
39
54
  }
40
55
  }, [location.search]);
41
56
  useEffect(() => {
42
- var _a;
43
57
  if (loading || !user)
44
58
  return;
45
- const requiresExtra = ((_a = user.security_state) === null || _a === void 0 ? void 0 : _a.requires_additional_security) === true;
46
- if (requiresExtra) {
47
- navigate('/account?tab=security&from=weak_login', { replace: true });
48
- }
49
- else {
50
- navigate('/', { replace: true });
51
- }
52
- }, [loading, user, navigate]);
59
+ navigate(getRedirectTarget(user), { replace: true });
60
+ }, [loading, user, navigate, requestedNext]);
53
61
  // --- Helper: Central Success Logic ---
54
62
  const handleLoginSuccess = (user) => {
55
- var _a;
56
63
  login(user); // Update Context
57
- // Check if "Strong Security" is enforced/required but not met
58
- const requiresExtra = ((_a = user.security_state) === null || _a === void 0 ? void 0 : _a.requires_additional_security) === true;
59
- if (requiresExtra) {
60
- navigate('/account?tab=security&from=weak_login');
61
- }
62
- else {
63
- // Standard Redirect (könnte man noch mit ?next=... erweitern)
64
- navigate('/');
65
- }
64
+ navigate(getRedirectTarget(user));
66
65
  };
67
66
  // --- Handlers ---
68
67
  const handleSubmitCredentials = async ({ identifier, password }) => {
@@ -72,9 +71,10 @@ export function LoginPage() {
72
71
  // A) Recovery Flow
73
72
  if (recoveryToken) {
74
73
  const result = await loginWithRecoveryPassword(identifier, password, recoveryToken);
75
- // Recovery login implies a specific redirect usually, usually straight to security settings
76
74
  login(result.user);
77
- navigate('/account?tab=security&from=recovery');
75
+ navigate(getRedirectTarget(result.user, {
76
+ forceSecurityRedirect: '/account?tab=security&from=recovery',
77
+ }));
78
78
  return;
79
79
  }
80
80
  // B) Standard Password Login
@@ -118,9 +118,10 @@ export function LoginPage() {
118
118
  const handleMfaSuccess = ({ user, method }) => {
119
119
  // MFA component should return the user object after verifying code
120
120
  if (method === 'recovery_code') {
121
- // Recovery codes often trigger a security check prompt
122
121
  login(user);
123
- navigate('/account?tab=security&from=recovery');
122
+ navigate(getRedirectTarget(user, {
123
+ forceSecurityRedirect: '/account?tab=security&from=recovery',
124
+ }));
124
125
  }
125
126
  else {
126
127
  handleLoginSuccess(user);
@@ -13,6 +13,8 @@ export function PasswordInvitePage() {
13
13
  const location = useLocation();
14
14
  const navigate = useNavigate();
15
15
  const { t } = useTranslation();
16
+ const searchParams = new URLSearchParams(location.search);
17
+ const nextPath = searchParams.get('next');
16
18
  const [submitting, setSubmitting] = useState(false);
17
19
  const [errorKey, setErrorKey] = useState(null);
18
20
  const [successKey, setSuccessKey] = useState(null);
@@ -57,7 +59,10 @@ export function PasswordInvitePage() {
57
59
  setSuccessKey(isInvite
58
60
  ? 'Auth.RESET_PASSWORD_SUCCESS_INVITE'
59
61
  : 'Auth.RESET_PASSWORD_SUCCESS_RESET');
60
- navigate('/login');
62
+ const target = nextPath
63
+ ? `/login?next=${encodeURIComponent(nextPath)}`
64
+ : '/login';
65
+ navigate(target);
61
66
  }
62
67
  catch (err) {
63
68
  setErrorKey(err.code || 'Auth.RESET_PASSWORD_FAILED');
@@ -1,7 +1,7 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import React, { useContext, useEffect, useMemo, useState } from 'react';
3
3
  import { useNavigate, useLocation } from 'react-router-dom';
4
- import { Alert, Box, Button, Stack, Tab, Tabs, TextField, Typography, } from '@mui/material';
4
+ import { Alert, Box, Button, Stack, TextField, Typography, } from '@mui/material';
5
5
  import { Helmet } from 'react-helmet';
6
6
  import { useTranslation } from 'react-i18next';
7
7
  import { NarrowPage } from '../layout/PageLayout';
@@ -13,6 +13,12 @@ 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_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
+ };
16
22
  export function SignUpPage() {
17
23
  const navigate = useNavigate();
18
24
  const location = useLocation();
@@ -41,18 +47,15 @@ export function SignUpPage() {
41
47
  const [submitting, setSubmitting] = useState(false);
42
48
  const [successKey, setSuccessKey] = useState(null);
43
49
  const [errorKey, setErrorKey] = useState(null);
44
- const [qrHint, setQrHint] = useState('');
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.');
53
+ }
54
+ return t(MODE_SUBTITLES[mode] || 'Auth.PAGE_SIGNUP_SUBTITLE', 'Choose a sign-up method and enter the required details.');
55
+ }, [mode, t, tokenFromUrl]);
45
56
  useEffect(() => {
46
57
  setMode(initialMode);
47
58
  }, [initialMode]);
48
- useEffect(() => {
49
- if (!tokenFromUrl || mode !== 'self_signup_qr') {
50
- setQrHint('');
51
- return undefined;
52
- }
53
- setQrHint(t('Auth.SIGNUP_QR_READY', 'QR signup token detected. You can complete your signup now.'));
54
- return undefined;
55
- }, [mode, tokenFromUrl, t]);
56
59
  const handleSubmit = async (event) => {
57
60
  event.preventDefault();
58
61
  setSuccessKey(null);
@@ -89,7 +92,7 @@ export function SignUpPage() {
89
92
  const handleGoToLogin = () => {
90
93
  navigate('/login');
91
94
  };
92
- 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 && (_jsx(Tabs, { value: mode, onChange: (_event, next) => setMode(next), variant: "scrollable", scrollButtons: "auto", sx: { mb: 2 }, children: signupModes.map((entry) => (_jsx(Tab, { value: entry, label: 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_email_domain' && (_jsx(Alert, { severity: "info", children: t('Auth.SIGNUP_EMAIL_DOMAIN_HINT', 'Only addresses from configured email domains are allowed for this signup flow.') })), mode === 'self_signup_qr' && (_jsxs(Stack, { spacing: 1, children: [_jsx(Alert, { severity: "info", children: qrHint || t('Auth.SIGNUP_QR_HINT', 'Use a valid QR signup link to continue.') }), _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 })), 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
93
96
  ? t('Auth.SIGNUP_SUBMITTING')
94
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') })] }) })] }));
95
98
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "private": false,
@@ -23,15 +23,6 @@ export async function fetchAuthMethods() {
23
23
  return res.data || {};
24
24
  }
25
25
 
26
- export async function fetchRegistrationOptions() {
27
- try {
28
- const res = await apiClient.get(`${USERS_BASE}/registration-options/`);
29
- return res.data || {};
30
- } catch (error) {
31
- throw normaliseApiError(error, 'Auth.REGISTRATION_OPTIONS_FAILED');
32
- }
33
- }
34
-
35
26
  export async function fetchAuthPolicy() {
36
27
  try {
37
28
  const res = await apiClient.get(`${USERS_BASE}/auth-policy/`);
@@ -390,17 +381,6 @@ export async function createSignupQr(payload = {}) {
390
381
  }
391
382
  }
392
383
 
393
- export async function requestInviteWithCode(email, accessCode) {
394
- if (accessCode) {
395
- return submitRegistrationRequest({
396
- email,
397
- mode: 'self_signup_access_code',
398
- accessCode,
399
- });
400
- }
401
- return sendAdminInvite(email);
402
- }
403
-
404
384
  // -----------------------------
405
385
  // Recovery Support (Admin/Support Side)
406
386
  // -----------------------------
@@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react';
2
2
  import {
3
3
  Alert,
4
4
  Box,
5
- Button,
6
5
  FormControl,
7
6
  FormControlLabel,
8
7
  Radio,
@@ -36,14 +35,18 @@ export function AuthFactorRequirementCard() {
36
35
  };
37
36
  }, []);
38
37
 
39
- const save = async () => {
38
+ const handleChange = async (event) => {
39
+ const nextValue = event.target.value;
40
+ const previous = value;
41
+ setValue(nextValue);
40
42
  setBusy(true);
41
43
  setError('');
42
44
  setSuccess('');
43
45
  try {
44
- await updateAuthPolicy({ required_auth_factor_count: Number(value) });
46
+ await updateAuthPolicy({ required_auth_factor_count: Number(nextValue) });
45
47
  setSuccess(t('Auth.AUTH_FACTOR_SAVE_SUCCESS', 'Factor requirement saved.'));
46
48
  } catch (err) {
49
+ setValue(previous);
47
50
  setError(t(err?.code || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save factor requirement.'));
48
51
  } finally {
49
52
  setBusy(false);
@@ -61,14 +64,11 @@ export function AuthFactorRequirementCard() {
61
64
  {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
62
65
  {success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
63
66
  <FormControl>
64
- <RadioGroup value={value} onChange={(event) => setValue(event.target.value)}>
65
- <FormControlLabel value="1" control={<Radio />} label={t('Auth.ONE_FACTOR_LABEL', 'One factor is enough')} />
66
- <FormControlLabel value="2" control={<Radio />} label={t('Auth.TWO_FACTOR_LABEL', 'Two factors are required')} />
67
+ <RadioGroup value={value} onChange={handleChange}>
68
+ <FormControlLabel value="1" control={<Radio disabled={busy} />} label={t('Auth.ONE_FACTOR_LABEL', 'One factor is enough')} />
69
+ <FormControlLabel value="2" control={<Radio disabled={busy} />} label={t('Auth.TWO_FACTOR_LABEL', 'Two factors are required')} />
67
70
  </RadioGroup>
68
71
  </FormControl>
69
- <Button variant="contained" sx={{ mt: 2 }} onClick={save} disabled={busy}>
70
- {t('Common.SAVE', 'Save')}
71
- </Button>
72
72
  </Box>
73
73
  );
74
74
  }