@micha.bigler/ui-core-micha 2.1.19 → 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 +20 -3
- 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 +24 -2
- package/src/pages/SignUpPage.jsx +120 -30
package/src/pages/SignUpPage.jsx
CHANGED
|
@@ -1,27 +1,74 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import { useNavigate } from 'react-router-dom';
|
|
1
|
+
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { useNavigate, useLocation } from 'react-router-dom';
|
|
4
3
|
import {
|
|
4
|
+
Alert,
|
|
5
5
|
Box,
|
|
6
|
-
TextField,
|
|
7
6
|
Button,
|
|
7
|
+
Stack,
|
|
8
|
+
Tab,
|
|
9
|
+
Tabs,
|
|
10
|
+
TextField,
|
|
8
11
|
Typography,
|
|
9
|
-
Alert,
|
|
10
12
|
} from '@mui/material';
|
|
11
13
|
import { Helmet } from 'react-helmet';
|
|
12
14
|
import { useTranslation } from 'react-i18next';
|
|
13
15
|
import { NarrowPage } from '../layout/PageLayout';
|
|
14
|
-
import {
|
|
16
|
+
import { AuthContext } from '../auth/AuthContext';
|
|
17
|
+
import { submitRegistrationRequest } from '../auth/authApi';
|
|
18
|
+
|
|
19
|
+
const MODE_LABELS = {
|
|
20
|
+
self_signup_access_code: 'Auth.SIGNUP_ACCESS_CODE_TAB',
|
|
21
|
+
self_signup_open: 'Auth.SIGNUP_OPEN_TAB',
|
|
22
|
+
self_signup_email_domain: 'Auth.SIGNUP_EMAIL_DOMAIN_TAB',
|
|
23
|
+
self_signup_qr: 'Auth.SIGNUP_QR_TAB',
|
|
24
|
+
};
|
|
15
25
|
|
|
16
26
|
export function SignUpPage() {
|
|
17
27
|
const navigate = useNavigate();
|
|
28
|
+
const location = useLocation();
|
|
18
29
|
const { t } = useTranslation();
|
|
30
|
+
const { authMethods } = useContext(AuthContext);
|
|
31
|
+
|
|
32
|
+
const signupModes = useMemo(() => {
|
|
33
|
+
const configured = Array.isArray(authMethods?.signup_modes)
|
|
34
|
+
? authMethods.signup_modes.filter(Boolean)
|
|
35
|
+
: [];
|
|
36
|
+
if (configured.length > 0) {
|
|
37
|
+
return configured;
|
|
38
|
+
}
|
|
39
|
+
return authMethods?.signup ? ['self_signup_access_code'] : [];
|
|
40
|
+
}, [authMethods]);
|
|
41
|
+
|
|
42
|
+
const query = new URLSearchParams(location.search);
|
|
43
|
+
const tokenFromUrl = query.get('rt') || '';
|
|
19
44
|
|
|
45
|
+
const initialMode = useMemo(() => {
|
|
46
|
+
if (tokenFromUrl && signupModes.includes('self_signup_qr')) {
|
|
47
|
+
return 'self_signup_qr';
|
|
48
|
+
}
|
|
49
|
+
return signupModes[0] || 'self_signup_access_code';
|
|
50
|
+
}, [signupModes, tokenFromUrl]);
|
|
51
|
+
|
|
52
|
+
const [mode, setMode] = useState(initialMode);
|
|
20
53
|
const [email, setEmail] = useState('');
|
|
21
54
|
const [accessCode, setAccessCode] = useState('');
|
|
22
55
|
const [submitting, setSubmitting] = useState(false);
|
|
23
56
|
const [successKey, setSuccessKey] = useState(null);
|
|
24
57
|
const [errorKey, setErrorKey] = useState(null);
|
|
58
|
+
const [qrHint, setQrHint] = useState('');
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
setMode(initialMode);
|
|
62
|
+
}, [initialMode]);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!tokenFromUrl || mode !== 'self_signup_qr') {
|
|
66
|
+
setQrHint('');
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
setQrHint(t('Auth.SIGNUP_QR_READY', 'QR signup token detected. You can complete your signup now.'));
|
|
70
|
+
return undefined;
|
|
71
|
+
}, [mode, tokenFromUrl, t]);
|
|
25
72
|
|
|
26
73
|
const handleSubmit = async (event) => {
|
|
27
74
|
event.preventDefault();
|
|
@@ -32,27 +79,27 @@ export function SignUpPage() {
|
|
|
32
79
|
setErrorKey('Auth.EMAIL_REQUIRED');
|
|
33
80
|
return;
|
|
34
81
|
}
|
|
35
|
-
|
|
82
|
+
|
|
83
|
+
if (mode === 'self_signup_access_code' && !accessCode) {
|
|
36
84
|
setErrorKey('Auth.SIGNUP_ACCESS_CODE_REQUIRED');
|
|
37
85
|
return;
|
|
38
86
|
}
|
|
39
87
|
|
|
40
|
-
|
|
88
|
+
if (mode === 'self_signup_qr' && !tokenFromUrl) {
|
|
89
|
+
setErrorKey('Auth.SIGNUP_QR_INVALID');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
41
92
|
|
|
93
|
+
setSubmitting(true);
|
|
42
94
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// 2) Invite anfordern
|
|
51
|
-
await requestInviteWithCode(email, accessCode);
|
|
52
|
-
|
|
95
|
+
await submitRegistrationRequest({
|
|
96
|
+
email,
|
|
97
|
+
mode,
|
|
98
|
+
accessCode,
|
|
99
|
+
registrationContextToken: mode === 'self_signup_qr' ? tokenFromUrl : null,
|
|
100
|
+
});
|
|
53
101
|
setSuccessKey('Auth.INVITE_REQUEST_SUCCESS');
|
|
54
102
|
} catch (err) {
|
|
55
|
-
// validateAccessCode / requestInviteWithCode liefern normalisierte Errors
|
|
56
103
|
setErrorKey(err.code || 'Auth.INVITE_FAILED');
|
|
57
104
|
} finally {
|
|
58
105
|
setSubmitting(false);
|
|
@@ -82,10 +129,28 @@ export function SignUpPage() {
|
|
|
82
129
|
|
|
83
130
|
{errorKey && (
|
|
84
131
|
<Alert severity="error" sx={{ mb: 2 }}>
|
|
85
|
-
{t(errorKey)}
|
|
132
|
+
{t(errorKey, t('Auth.INVITE_FAILED', 'Could not complete signup.'))}
|
|
86
133
|
</Alert>
|
|
87
134
|
)}
|
|
88
135
|
|
|
136
|
+
{signupModes.length > 1 && (
|
|
137
|
+
<Tabs
|
|
138
|
+
value={mode}
|
|
139
|
+
onChange={(_event, next) => setMode(next)}
|
|
140
|
+
variant="scrollable"
|
|
141
|
+
scrollButtons="auto"
|
|
142
|
+
sx={{ mb: 2 }}
|
|
143
|
+
>
|
|
144
|
+
{signupModes.map((entry) => (
|
|
145
|
+
<Tab
|
|
146
|
+
key={entry}
|
|
147
|
+
value={entry}
|
|
148
|
+
label={t(MODE_LABELS[entry] || entry, entry)}
|
|
149
|
+
/>
|
|
150
|
+
))}
|
|
151
|
+
</Tabs>
|
|
152
|
+
)}
|
|
153
|
+
|
|
89
154
|
<Box
|
|
90
155
|
component="form"
|
|
91
156
|
onSubmit={handleSubmit}
|
|
@@ -101,20 +166,45 @@ export function SignUpPage() {
|
|
|
101
166
|
disabled={submitting}
|
|
102
167
|
/>
|
|
103
168
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
169
|
+
{mode === 'self_signup_access_code' && (
|
|
170
|
+
<TextField
|
|
171
|
+
label={t('Auth.ACCESS_CODE_LABEL')}
|
|
172
|
+
type="text"
|
|
173
|
+
required
|
|
174
|
+
fullWidth
|
|
175
|
+
value={accessCode}
|
|
176
|
+
onChange={(e) => setAccessCode(e.target.value)}
|
|
177
|
+
disabled={submitting}
|
|
178
|
+
/>
|
|
179
|
+
)}
|
|
180
|
+
|
|
181
|
+
{mode === 'self_signup_email_domain' && (
|
|
182
|
+
<Alert severity="info">
|
|
183
|
+
{t(
|
|
184
|
+
'Auth.SIGNUP_EMAIL_DOMAIN_HINT',
|
|
185
|
+
'Only addresses from configured email domains are allowed for this signup flow.',
|
|
186
|
+
)}
|
|
187
|
+
</Alert>
|
|
188
|
+
)}
|
|
189
|
+
|
|
190
|
+
{mode === 'self_signup_qr' && (
|
|
191
|
+
<Stack spacing={1}>
|
|
192
|
+
<Alert severity="info">
|
|
193
|
+
{qrHint || t('Auth.SIGNUP_QR_HINT', 'Use a valid QR signup link to continue.')}
|
|
194
|
+
</Alert>
|
|
195
|
+
<TextField
|
|
196
|
+
label={t('Auth.SIGNUP_QR_TOKEN_LABEL', 'QR token')}
|
|
197
|
+
value={tokenFromUrl}
|
|
198
|
+
fullWidth
|
|
199
|
+
InputProps={{ readOnly: true }}
|
|
200
|
+
/>
|
|
201
|
+
</Stack>
|
|
202
|
+
)}
|
|
113
203
|
|
|
114
204
|
<Button
|
|
115
205
|
type="submit"
|
|
116
206
|
variant="contained"
|
|
117
|
-
disabled={submitting}
|
|
207
|
+
disabled={submitting || signupModes.length === 0}
|
|
118
208
|
>
|
|
119
209
|
{submitting
|
|
120
210
|
? t('Auth.SIGNUP_SUBMITTING')
|