@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.
- package/dist/auth/AuthContext.js +4 -0
- package/dist/auth/authApi.js +64 -3
- package/dist/components/AccessCodeManager.js +39 -3
- package/dist/components/AuthFactorRequirementCard.js +45 -0
- package/dist/components/BulkInviteCsvTab.js +2 -2
- package/dist/components/QrSignupManager.js +43 -0
- package/dist/components/RegistrationMethodsManager.js +69 -0
- package/dist/components/UserInviteComponent.js +2 -4
- package/dist/components/UserListComponent.js +130 -105
- package/dist/index.js +3 -0
- package/dist/pages/AccountPage.js +4 -1
- package/dist/pages/LoginPage.js +7 -2
- package/dist/pages/SignUpPage.js +55 -16
- package/package.json +2 -1
- package/src/auth/AuthContext.jsx +4 -0
- package/src/auth/authApi.jsx +71 -3
- package/src/components/AccessCodeManager.jsx +71 -8
- package/src/components/AuthFactorRequirementCard.jsx +74 -0
- package/src/components/BulkInviteCsvTab.jsx +2 -2
- package/src/components/QrSignupManager.jsx +84 -0
- package/src/components/RegistrationMethodsManager.jsx +127 -0
- package/src/components/UserInviteComponent.jsx +2 -4
- package/src/components/UserListComponent.jsx +216 -246
- package/src/index.js +3 -0
- package/src/pages/AccountPage.jsx +21 -0
- package/src/pages/LoginPage.jsx +12 -1
- package/src/pages/SignUpPage.jsx +120 -30
package/dist/auth/AuthContext.js
CHANGED
|
@@ -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);
|
package/dist/auth/authApi.js
CHANGED
|
@@ -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
|
|
313
|
-
|
|
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}/
|
|
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,
|
|
5
|
-
import
|
|
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) })),
|
|
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 {
|
|
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) =>
|
|
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 {
|
|
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
|
-
|
|
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
|
}
|