@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.
- package/dist/auth/AuthContext.js +4 -0
- package/dist/auth/authApi.js +45 -3
- package/dist/components/AccessCodeManager.js +39 -3
- package/dist/components/AuthFactorRequirementCard.js +49 -0
- package/dist/components/BulkInviteCsvTab.js +2 -2
- package/dist/components/LoginForm.js +1 -1
- package/dist/components/QrSignupManager.js +81 -0
- package/dist/components/RegistrationMethodsManager.js +91 -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 +6 -2
- package/dist/pages/LoginPage.js +31 -25
- package/dist/pages/PasswordInvitePage.js +6 -1
- package/dist/pages/SignUpPage.js +76 -16
- package/package.json +2 -1
- package/src/auth/AuthContext.jsx +4 -0
- package/src/auth/authApi.jsx +51 -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/LoginForm.jsx +7 -6
- package/src/components/QrSignupManager.jsx +128 -0
- package/src/components/RegistrationMethodsManager.jsx +184 -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 +23 -1
- package/src/pages/LoginPage.jsx +43 -23
- package/src/pages/PasswordInvitePage.jsx +6 -1
- package/src/pages/SignUpPage.jsx +145 -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,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
|
|
313
|
-
|
|
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}/
|
|
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,
|
|
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,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 {
|
|
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,
|
|
@@ -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(
|
|
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 {
|
|
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
|
}
|