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

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,33 @@ 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
+ export async function fetchAuthPolicy() {
32
+ try {
33
+ const res = await apiClient.get(`${USERS_BASE}/auth-policy/`);
34
+ return res.data || {};
35
+ }
36
+ catch (error) {
37
+ throw normaliseApiError(error, 'Auth.AUTH_POLICY_FETCH_FAILED');
38
+ }
39
+ }
40
+ export async function updateAuthPolicy(payload) {
41
+ try {
42
+ const res = await apiClient.patch(`${USERS_BASE}/auth-policy/`, payload);
43
+ return res.data || {};
44
+ }
45
+ catch (error) {
46
+ throw normaliseApiError(error, 'Auth.AUTH_POLICY_UPDATE_FAILED');
47
+ }
48
+ }
22
49
  export async function updateUserProfile(data) {
23
50
  try {
24
51
  const res = await apiClient.patch(`${USERS_BASE}/current/`, data);
@@ -309,18 +336,52 @@ export async function validateAccessCode(code) {
309
336
  throw normaliseApiError(error, 'Auth.ACCESS_CODE_INVALID_OR_INACTIVE');
310
337
  }
311
338
  }
312
- export async function requestInviteWithCode(email, accessCode) {
313
- const payload = { email };
339
+ export async function sendAdminInvite(email) {
340
+ try {
341
+ const res = await apiClient.post(`${USERS_BASE}/invite/`, { email });
342
+ return res.data;
343
+ }
344
+ catch (error) {
345
+ throw normaliseApiError(error, 'Auth.INVITE_FAILED');
346
+ }
347
+ }
348
+ export async function submitRegistrationRequest({ email, mode, accessCode, registrationContextToken, registrationContext, }) {
349
+ const payload = { email, mode };
314
350
  if (accessCode)
315
351
  payload.access_code = accessCode;
352
+ if (registrationContextToken) {
353
+ payload.registration_context_token = registrationContextToken;
354
+ }
355
+ if (registrationContext) {
356
+ payload.registration_context = registrationContext;
357
+ }
316
358
  try {
317
- const res = await apiClient.post(`${USERS_BASE}/invite/`, payload);
359
+ const res = await apiClient.post(`${USERS_BASE}/register-request/`, payload);
318
360
  return res.data;
319
361
  }
320
362
  catch (error) {
321
363
  throw normaliseApiError(error, 'Auth.INVITE_FAILED');
322
364
  }
323
365
  }
366
+ export async function createSignupQr(payload = {}) {
367
+ try {
368
+ const res = await apiClient.post(`${USERS_BASE}/signup-qr/`, payload);
369
+ return res.data || {};
370
+ }
371
+ catch (error) {
372
+ throw normaliseApiError(error, 'Auth.SIGNUP_QR_CREATE_FAILED');
373
+ }
374
+ }
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
+ }
324
385
  // -----------------------------
325
386
  // Recovery Support (Admin/Support Side)
326
387
  // -----------------------------
@@ -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,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useEffect, useState } from 'react';
3
+ import { Alert, Box, Button, 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 save = async () => {
30
+ setBusy(true);
31
+ setError('');
32
+ setSuccess('');
33
+ try {
34
+ await updateAuthPolicy({ required_auth_factor_count: Number(value) });
35
+ setSuccess(t('Auth.AUTH_FACTOR_SAVE_SUCCESS', 'Factor requirement saved.'));
36
+ }
37
+ catch (err) {
38
+ setError(t((err === null || err === void 0 ? void 0 : err.code) || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save factor requirement.'));
39
+ }
40
+ finally {
41
+ setBusy(false);
42
+ }
43
+ };
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') })] }));
45
+ }
@@ -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,
@@ -0,0 +1,43 @@
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';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { QRCodeSVG } from 'qrcode.react';
6
+ import { createSignupQr } from '../auth/authApi';
7
+ export function QrSignupManager() {
8
+ const { t } = useTranslation();
9
+ const [label, setLabel] = useState('');
10
+ const [eventRef, setEventRef] = useState('');
11
+ const [courseRef, setCourseRef] = useState('');
12
+ const [groupRef, setGroupRef] = useState('');
13
+ const [busy, setBusy] = useState(false);
14
+ const [error, setError] = useState('');
15
+ const [result, setResult] = useState(null);
16
+ const generate = async () => {
17
+ setBusy(true);
18
+ setError('');
19
+ 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
+ const data = await createSignupQr({
30
+ label,
31
+ registration_context: registrationContext,
32
+ });
33
+ setResult(data);
34
+ }
35
+ catch (err) {
36
+ setError(t((err === null || err === void 0 ? void 0 : err.code) || 'Auth.SIGNUP_QR_CREATE_FAILED', 'Could not create signup QR.'));
37
+ }
38
+ finally {
39
+ setBusy(false);
40
+ }
41
+ };
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 } })] }))] }));
43
+ }
@@ -0,0 +1,69 @@
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 [error, setError] = useState('');
21
+ const [success, setSuccess] = useState('');
22
+ useEffect(() => {
23
+ let active = true;
24
+ (async () => {
25
+ try {
26
+ const data = await fetchAuthPolicy();
27
+ if (!active)
28
+ return;
29
+ setPolicy((prev) => (Object.assign(Object.assign({}, prev), data)));
30
+ setDomainsText(((data === null || data === void 0 ? void 0 : data.allowed_email_domains) || []).join('\n'));
31
+ }
32
+ catch (err) {
33
+ if (active) {
34
+ setError(t((err === null || err === void 0 ? void 0 : err.code) || 'Auth.AUTH_POLICY_FETCH_FAILED', 'Could not load authentication policy.'));
35
+ }
36
+ }
37
+ })();
38
+ return () => {
39
+ active = false;
40
+ };
41
+ }, [t]);
42
+ const toggle = (field) => (_event, checked) => {
43
+ setPolicy((prev) => (Object.assign(Object.assign({}, prev), { [field]: checked })));
44
+ };
45
+ const save = async () => {
46
+ setBusy(true);
47
+ setError('');
48
+ setSuccess('');
49
+ try {
50
+ const allowed_email_domains = domainsText
51
+ .split(/\r?\n/)
52
+ .map((value) => value.trim())
53
+ .filter(Boolean);
54
+ const next = await updateAuthPolicy(Object.assign(Object.assign({}, policy), { allowed_email_domains }));
55
+ setPolicy((prev) => (Object.assign(Object.assign({}, prev), next)));
56
+ setDomainsText(((next === null || next === void 0 ? void 0 : next.allowed_email_domains) || allowed_email_domains).join('\n'));
57
+ setSuccess(t('Auth.AUTH_POLICY_SAVE_SUCCESS', 'Authentication settings saved.'));
58
+ if (onPolicyChange)
59
+ onPolicyChange(next);
60
+ }
61
+ catch (err) {
62
+ setError(t((err === null || err === void 0 ? void 0 : err.code) || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save authentication settings.'));
63
+ }
64
+ finally {
65
+ setBusy(false);
66
+ }
67
+ };
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') })] }));
69
+ }
@@ -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
  }