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

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.
@@ -8,6 +8,7 @@ const DEFAULT_AUTH_METHODS = {
8
8
  password_login: true,
9
9
  password_reset: true,
10
10
  signup: true,
11
+ signup_modes: ['self_signup_access_code'],
11
12
  password_change: true,
12
13
  social_login: true,
13
14
  social_providers: ['google', 'microsoft'],
@@ -16,6 +17,9 @@ const DEFAULT_AUTH_METHODS = {
16
17
  mfa_totp: true,
17
18
  mfa_recovery_codes: true,
18
19
  mfa_enabled: true,
20
+ required_auth_factor_count: 1,
21
+ two_factor_required: false,
22
+ qr_signup_enabled: false,
19
23
  };
20
24
  export const AuthProvider = ({ children }) => {
21
25
  const [user, setUser] = useState(null);
@@ -19,6 +19,24 @@ 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 fetchAuthPolicy() {
23
+ try {
24
+ const res = await apiClient.get(`${USERS_BASE}/auth-policy/`);
25
+ return res.data || {};
26
+ }
27
+ catch (error) {
28
+ throw normaliseApiError(error, 'Auth.AUTH_POLICY_FETCH_FAILED');
29
+ }
30
+ }
31
+ export async function updateAuthPolicy(payload) {
32
+ try {
33
+ const res = await apiClient.patch(`${USERS_BASE}/auth-policy/`, payload);
34
+ return res.data || {};
35
+ }
36
+ catch (error) {
37
+ throw normaliseApiError(error, 'Auth.AUTH_POLICY_UPDATE_FAILED');
38
+ }
39
+ }
22
40
  export async function updateUserProfile(data) {
23
41
  try {
24
42
  const res = await apiClient.patch(`${USERS_BASE}/current/`, data);
@@ -309,18 +327,42 @@ export async function validateAccessCode(code) {
309
327
  throw normaliseApiError(error, 'Auth.ACCESS_CODE_INVALID_OR_INACTIVE');
310
328
  }
311
329
  }
312
- export async function requestInviteWithCode(email, accessCode) {
313
- const payload = { email };
330
+ export async function sendAdminInvite(email) {
331
+ try {
332
+ const res = await apiClient.post(`${USERS_BASE}/invite/`, { email });
333
+ return res.data;
334
+ }
335
+ catch (error) {
336
+ throw normaliseApiError(error, 'Auth.INVITE_FAILED');
337
+ }
338
+ }
339
+ export async function submitRegistrationRequest({ email, mode, accessCode, registrationContextToken, registrationContext, }) {
340
+ const payload = { email, mode };
314
341
  if (accessCode)
315
342
  payload.access_code = accessCode;
343
+ if (registrationContextToken) {
344
+ payload.registration_context_token = registrationContextToken;
345
+ }
346
+ if (registrationContext) {
347
+ payload.registration_context = registrationContext;
348
+ }
316
349
  try {
317
- const res = await apiClient.post(`${USERS_BASE}/invite/`, payload);
350
+ const res = await apiClient.post(`${USERS_BASE}/register-request/`, payload);
318
351
  return res.data;
319
352
  }
320
353
  catch (error) {
321
354
  throw normaliseApiError(error, 'Auth.INVITE_FAILED');
322
355
  }
323
356
  }
357
+ export async function createSignupQr(payload = {}) {
358
+ try {
359
+ const res = await apiClient.post(`${USERS_BASE}/signup-qr/`, payload);
360
+ return res.data || {};
361
+ }
362
+ catch (error) {
363
+ throw normaliseApiError(error, 'Auth.SIGNUP_QR_CREATE_FAILED');
364
+ }
365
+ }
324
366
  // -----------------------------
325
367
  // Recovery Support (Admin/Support Side)
326
368
  // -----------------------------
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // src/auth/components/AccessCodeManager.jsx
3
3
  import React, { useEffect, useState } from 'react';
4
- import { Box, Stack, Typography, Slider, Button, TextField, Chip, Alert, CircularProgress, } from '@mui/material';
5
- import CloseIcon from '@mui/icons-material/Close';
4
+ import { Box, Stack, Typography, Slider, Button, TextField, Alert, CircularProgress, } from '@mui/material';
5
+ import ContentCopyIcon from '@mui/icons-material/ContentCopy';
6
+ import DeleteIcon from '@mui/icons-material/Delete';
6
7
  import { useTranslation } from 'react-i18next';
7
8
  // Pfad ggf. anpassen je nach Struktur: von src/auth/components zu src/auth/authApi
8
9
  import { fetchAccessCodes, createAccessCode, deleteAccessCode, } from '../auth/authApi';
@@ -21,6 +22,7 @@ export function AccessCodeManager() {
21
22
  const [manualCode, setManualCode] = useState('');
22
23
  const [errorKey, setErrorKey] = useState(null);
23
24
  const [successKey, setSuccessKey] = useState(null);
25
+ const [copyNotice, setCopyNotice] = useState('');
24
26
  // Helper that prefers backend error code if available
25
27
  const setErrorFromErrorObject = (err, fallbackCode) => {
26
28
  const backendCode = err === null || err === void 0 ? void 0 : err.code;
@@ -93,10 +95,44 @@ export function AccessCodeManager() {
93
95
  setErrorFromErrorObject(err, 'Auth.ACCESS_CODE_DELETE_FAILED');
94
96
  }
95
97
  };
98
+ const handleCopyCode = async (codeValue) => {
99
+ var _a;
100
+ try {
101
+ if ((_a = navigator === null || navigator === void 0 ? void 0 : navigator.clipboard) === null || _a === void 0 ? void 0 : _a.writeText) {
102
+ await navigator.clipboard.writeText(codeValue);
103
+ }
104
+ else {
105
+ throw new Error('Clipboard API unavailable');
106
+ }
107
+ setCopyNotice(t('Auth.ACCESS_CODE_COPY_SUCCESS', 'Code kopiert.'));
108
+ window.setTimeout(() => setCopyNotice(''), 1800);
109
+ }
110
+ catch (_err) {
111
+ setCopyNotice(t('Auth.ACCESS_CODE_COPY_FALLBACK', 'Code markieren und mit Ctrl+C kopieren.'));
112
+ window.setTimeout(() => setCopyNotice(''), 2200);
113
+ }
114
+ };
96
115
  if (loading) {
97
116
  return (_jsx(Box, { sx: { py: 3, display: 'flex', justifyContent: 'center' }, children: _jsx(CircularProgress, {}) }));
98
117
  }
99
- return (_jsxs(Box, { children: [errorKey && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(errorKey) })), successKey && (_jsx(Alert, { severity: "success", sx: { mb: 2 }, children: t(successKey) })), _jsxs(Box, { sx: { mb: 3 }, children: [_jsx(Typography, { variant: "subtitle1", gutterBottom: true, children: t('Auth.ACCESS_CODE_SECTION_ACTIVE') }), codes.length === 0 ? (_jsx(Typography, { variant: "body2", children: t('Auth.ACCESS_CODE_NONE') })) : (_jsx(Stack, { direction: "row", flexWrap: "wrap", gap: 1, children: codes.map((code) => (_jsx(Chip, { label: code.code, onDelete: () => handleDelete(code.id), deleteIcon: _jsx(CloseIcon, {}) }, code.id))) }))] }), _jsxs(Box, { sx: { mb: 3 }, children: [_jsx(Typography, { variant: "subtitle1", gutterBottom: true, children: t('Auth.ACCESS_CODE_SECTION_GENERATE') }), _jsxs(Box, { sx: { maxWidth: 360 }, children: [_jsx(Typography, { variant: "body2", gutterBottom: true, children: t('Auth.ACCESS_CODE_LENGTH_LABEL', { length }) }), _jsx(Slider, { min: 6, max: 32, step: 1, value: length, onChange: (_, val) => setLength(val), valueLabelDisplay: "auto", disabled: submitting })] }), _jsx(Button, { variant: "contained", size: "small", sx: Object.assign(Object.assign({}, actionButtonSx), { mt: 1 }), onClick: handleGenerateClick, disabled: submitting, children: submitting
118
+ return (_jsxs(Box, { children: [errorKey && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(errorKey) })), successKey && (_jsx(Alert, { severity: "success", sx: { mb: 2 }, children: t(successKey) })), copyNotice && (_jsx(Alert, { severity: "info", sx: { mb: 2 }, children: copyNotice })), _jsxs(Box, { sx: { mb: 3 }, children: [_jsx(Typography, { variant: "subtitle1", gutterBottom: true, children: t('Auth.ACCESS_CODE_SECTION_ACTIVE') }), codes.length === 0 ? (_jsx(Typography, { variant: "body2", children: t('Auth.ACCESS_CODE_NONE') })) : (_jsx(Stack, { spacing: 1, children: codes.map((code) => (_jsxs(Box, { sx: {
119
+ display: 'grid',
120
+ gridTemplateColumns: '1fr auto auto',
121
+ gap: 1,
122
+ alignItems: 'center',
123
+ width: '100%',
124
+ maxWidth: 560,
125
+ }, children: [_jsx(TextField, { value: code.code, size: "small", fullWidth: true, slotProps: {
126
+ input: {
127
+ readOnly: true,
128
+ onFocus: (event) => event.target.select(),
129
+ },
130
+ }, sx: {
131
+ '& .MuiInputBase-input': {
132
+ fontFamily: 'monospace',
133
+ letterSpacing: '0.04em',
134
+ },
135
+ } }), _jsx(Button, { variant: "outlined", size: "small", onClick: () => handleCopyCode(code.code), startIcon: _jsx(ContentCopyIcon, { fontSize: "small" }), sx: actionButtonSx, children: t('Auth.ACCESS_CODE_COPY_BUTTON', t('Auth.MFA_RECOVERY_COPY_TOOLTIP', 'Kopieren')) }), _jsx(Button, { variant: "outlined", color: "error", size: "small", onClick: () => handleDelete(code.id), startIcon: _jsx(DeleteIcon, { fontSize: "small" }), sx: actionButtonSx, children: t('Common.DELETE', 'Löschen') })] }, code.id))) }))] }), _jsxs(Box, { sx: { mb: 3 }, children: [_jsx(Typography, { variant: "subtitle1", gutterBottom: true, children: t('Auth.ACCESS_CODE_SECTION_GENERATE') }), _jsxs(Box, { sx: { maxWidth: 360 }, children: [_jsx(Typography, { variant: "body2", gutterBottom: true, children: t('Auth.ACCESS_CODE_LENGTH_LABEL', { length }) }), _jsx(Slider, { min: 6, max: 32, step: 1, value: length, onChange: (_, val) => setLength(val), valueLabelDisplay: "auto", disabled: submitting })] }), _jsx(Button, { variant: "contained", size: "small", sx: Object.assign(Object.assign({}, actionButtonSx), { mt: 1 }), onClick: handleGenerateClick, disabled: submitting, children: submitting
100
136
  ? t('Auth.SAVE_BUTTON_LOADING')
101
137
  : t('Auth.ACCESS_CODE_GENERATE_BUTTON') })] }), _jsxs(Box, { sx: { mb: 2, maxWidth: 360 }, children: [_jsx(Typography, { variant: "subtitle1", gutterBottom: true, children: t('Auth.ACCESS_CODE_SECTION_MANUAL') }), _jsxs(Box, { sx: { display: 'flex', gap: 1, alignItems: 'center' }, children: [_jsx(TextField, { label: t('Auth.ACCESS_CODE_LABEL'), fullWidth: true, size: "small", value: manualCode, onChange: (e) => setManualCode(e.target.value), disabled: submitting }), _jsx(Button, { variant: "contained", size: "small", onClick: handleAddManual, disabled: submitting, sx: actionButtonSx, children: t('Auth.ACCESS_CODE_ADD_BUTTON') })] })] })] }));
102
138
  }
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useEffect, useState } from 'react';
3
+ import { Alert, Box, FormControl, FormControlLabel, Radio, RadioGroup, Typography, } from '@mui/material';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { fetchAuthPolicy, updateAuthPolicy } from '../auth/authApi';
6
+ export function AuthFactorRequirementCard() {
7
+ const { t } = useTranslation();
8
+ const [value, setValue] = useState('1');
9
+ const [busy, setBusy] = useState(false);
10
+ const [error, setError] = useState('');
11
+ const [success, setSuccess] = useState('');
12
+ useEffect(() => {
13
+ let active = true;
14
+ (async () => {
15
+ try {
16
+ const data = await fetchAuthPolicy();
17
+ if (active) {
18
+ setValue(String((data === null || data === void 0 ? void 0 : data.required_auth_factor_count) || 1));
19
+ }
20
+ }
21
+ catch (_a) {
22
+ // Keep defaults when policy is unavailable.
23
+ }
24
+ })();
25
+ return () => {
26
+ active = false;
27
+ };
28
+ }, []);
29
+ const handleChange = async (event) => {
30
+ const nextValue = event.target.value;
31
+ const previous = value;
32
+ setValue(nextValue);
33
+ setBusy(true);
34
+ setError('');
35
+ setSuccess('');
36
+ try {
37
+ await updateAuthPolicy({ required_auth_factor_count: Number(nextValue) });
38
+ setSuccess(t('Auth.AUTH_FACTOR_SAVE_SUCCESS', 'Factor requirement saved.'));
39
+ }
40
+ catch (err) {
41
+ setValue(previous);
42
+ setError(t((err === null || err === void 0 ? void 0 : err.code) || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save factor requirement.'));
43
+ }
44
+ finally {
45
+ setBusy(false);
46
+ }
47
+ };
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') })] }) })] }));
49
+ }
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useMemo, useState } from 'react';
3
3
  import { Box, Button, Typography, Alert, LinearProgress, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, } from '@mui/material';
4
4
  import { useTranslation } from 'react-i18next';
5
- import { requestInviteWithCode } from '../auth/authApi';
5
+ import { sendAdminInvite } from '../auth/authApi';
6
6
  function parseEmailsFromCsv(text) {
7
7
  if (!text)
8
8
  return [];
@@ -24,7 +24,7 @@ function parseEmailsFromCsv(text) {
24
24
  });
25
25
  return Array.from(new Set(emails.map((e) => e.toLowerCase())));
26
26
  }
27
- export function BulkInviteCsvTab({ inviteFn = (email) => requestInviteWithCode(email, null), onCompleted, }) {
27
+ export function BulkInviteCsvTab({ inviteFn = (email) => sendAdminInvite(email), onCompleted, }) {
28
28
  const { t } = useTranslation();
29
29
  const actionButtonSx = {
30
30
  minWidth: 120,
@@ -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
  ;
@@ -0,0 +1,81 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useEffect, useRef, useState } from 'react';
3
+ import { Alert, Box, Button, TextField, Typography, } from '@mui/material';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { QRCodeSVG } from 'qrcode.react';
6
+ import { createSignupQr } from '../auth/authApi';
7
+ export function QrSignupManager({ enabled = false }) {
8
+ const { t } = useTranslation();
9
+ const [label, setLabel] = useState('');
10
+ const [busy, setBusy] = useState(false);
11
+ const [error, setError] = useState('');
12
+ const [success, setSuccess] = useState('');
13
+ const [result, setResult] = useState(null);
14
+ const hasGeneratedRef = useRef(false);
15
+ const generate = async () => {
16
+ if (!enabled) {
17
+ setResult(null);
18
+ return;
19
+ }
20
+ setBusy(true);
21
+ setError('');
22
+ setSuccess('');
23
+ try {
24
+ const data = await createSignupQr({
25
+ label,
26
+ });
27
+ setResult(data);
28
+ hasGeneratedRef.current = true;
29
+ setSuccess(t('Auth.SIGNUP_QR_SAVE_SUCCESS', 'QR signup link updated.'));
30
+ }
31
+ catch (err) {
32
+ setError(t((err === null || err === void 0 ? void 0 : err.code) || 'Auth.SIGNUP_QR_CREATE_FAILED', 'Could not create signup QR.'));
33
+ }
34
+ finally {
35
+ setBusy(false);
36
+ }
37
+ };
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 } })] }))] }));
81
+ }
@@ -0,0 +1,91 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useEffect, useState } from 'react';
3
+ import { Alert, Box, Button, FormControlLabel, Stack, Switch, TextField, Typography, } from '@mui/material';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { fetchAuthPolicy, updateAuthPolicy } from '../auth/authApi';
6
+ const EMPTY_POLICY = {
7
+ allow_admin_invite: true,
8
+ allow_self_signup_access_code: false,
9
+ allow_self_signup_open: false,
10
+ allow_self_signup_email_domain: false,
11
+ allow_self_signup_qr: false,
12
+ allowed_email_domains: [],
13
+ required_auth_factor_count: 1,
14
+ };
15
+ export function RegistrationMethodsManager({ onPolicyChange }) {
16
+ const { t } = useTranslation();
17
+ const [policy, setPolicy] = useState(EMPTY_POLICY);
18
+ const [domainsText, setDomainsText] = useState('');
19
+ const [busy, setBusy] = useState(false);
20
+ const [busyField, setBusyField] = useState('');
21
+ const [error, setError] = useState('');
22
+ const [success, setSuccess] = useState('');
23
+ useEffect(() => {
24
+ let active = true;
25
+ (async () => {
26
+ try {
27
+ const data = await fetchAuthPolicy();
28
+ if (!active)
29
+ return;
30
+ setPolicy((prev) => (Object.assign(Object.assign({}, prev), data)));
31
+ setDomainsText(((data === null || data === void 0 ? void 0 : data.allowed_email_domains) || []).join('\n'));
32
+ if (onPolicyChange)
33
+ onPolicyChange(data);
34
+ }
35
+ catch (err) {
36
+ if (active) {
37
+ setError(t((err === null || err === void 0 ? void 0 : err.code) || 'Auth.AUTH_POLICY_FETCH_FAILED', 'Could not load authentication policy.'));
38
+ }
39
+ }
40
+ })();
41
+ return () => {
42
+ active = false;
43
+ };
44
+ }, [onPolicyChange, t]);
45
+ const toggle = (field) => async (_event, checked) => {
46
+ const previous = policy[field];
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
+ }
66
+ };
67
+ const save = async () => {
68
+ setBusy(true);
69
+ setError('');
70
+ setSuccess('');
71
+ try {
72
+ const allowed_email_domains = domainsText
73
+ .split(/\r?\n/)
74
+ .map((value) => value.trim())
75
+ .filter(Boolean);
76
+ const next = await updateAuthPolicy({ allowed_email_domains });
77
+ setPolicy((prev) => (Object.assign(Object.assign({}, prev), next)));
78
+ setDomainsText(((next === null || next === void 0 ? void 0 : next.allowed_email_domains) || allowed_email_domains).join('\n'));
79
+ setSuccess(t('Auth.AUTH_POLICY_SAVE_SUCCESS', 'Authentication settings saved.'));
80
+ if (onPolicyChange)
81
+ onPolicyChange(next);
82
+ }
83
+ catch (err) {
84
+ setError(t((err === null || err === void 0 ? void 0 : err.code) || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save authentication settings.'));
85
+ }
86
+ finally {
87
+ setBusy(false);
88
+ }
89
+ };
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') })] }));
91
+ }
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useState } from 'react';
3
3
  import { Box, TextField, Button, Typography, Alert, CircularProgress } from '@mui/material';
4
- import { requestInviteWithCode } from '../auth/authApi';
4
+ import { sendAdminInvite } from '../auth/authApi';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  export function UserInviteComponent() {
7
7
  const { t } = useTranslation();
@@ -22,9 +22,7 @@ export function UserInviteComponent() {
22
22
  return;
23
23
  setLoading(true);
24
24
  try {
25
- // FIX: 2nd parameter is accessCode. For admin invites without code, we pass null.
26
- // Previously, 'apiUrl' was passed here incorrectly.
27
- const data = await requestInviteWithCode(inviteEmail, null);
25
+ const data = await sendAdminInvite(inviteEmail);
28
26
  setInviteEmail('');
29
27
  setMessage(data.detail || t('Auth.INVITE_SENT_SUCCESS', 'Invitation sent.'));
30
28
  }