@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { useMemo, useState, useEffect } from 'react';
|
|
3
|
-
import { Box, Typography,
|
|
2
|
+
import React, { useMemo, useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import { Box, Typography, FormControl, Select, MenuItem, Button, Tooltip, CircularProgress, Alert, TextField, } from '@mui/material';
|
|
4
|
+
import { DataGrid } from '@mui/x-data-grid';
|
|
4
5
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|
5
6
|
import { useTranslation } from 'react-i18next';
|
|
6
7
|
import { fetchUsersList, deleteUser, updateUserRole, updateUserSupportStatus } from '../auth/authApi';
|
|
@@ -12,16 +13,12 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
12
13
|
const [error, setError] = useState(null);
|
|
13
14
|
const [rowActionLoading, setRowActionLoading] = useState({});
|
|
14
15
|
const [searchQuery, setSearchQuery] = useState('');
|
|
15
|
-
const [sortConfig, setSortConfig] = useState({ key: 'id', direction: 'asc' });
|
|
16
|
-
const cellSx = { verticalAlign: 'middle' };
|
|
17
16
|
const controlSx = { minWidth: 140 };
|
|
18
17
|
const actionButtonSx = { textTransform: 'none', minWidth: 90 };
|
|
19
|
-
const loadUsers = async () => {
|
|
18
|
+
const loadUsers = useCallback(async () => {
|
|
20
19
|
setLoading(true);
|
|
21
20
|
setError(null);
|
|
22
21
|
try {
|
|
23
|
-
// FIX: Removed apiUrl parameter.
|
|
24
|
-
// fetchUsersList uses USERS_BASE from authConfig internally.
|
|
25
22
|
const data = await fetchUsersList();
|
|
26
23
|
const list = Array.isArray(data) ? data : (Array.isArray(data === null || data === void 0 ? void 0 : data.results) ? data.results : []);
|
|
27
24
|
// Keep row identity stable even if backend returns unordered results.
|
|
@@ -39,16 +36,14 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
39
36
|
finally {
|
|
40
37
|
setLoading(false);
|
|
41
38
|
}
|
|
42
|
-
};
|
|
39
|
+
}, []);
|
|
43
40
|
useEffect(() => {
|
|
44
41
|
loadUsers();
|
|
45
|
-
|
|
46
|
-
}, [refreshTrigger]); // Dependency on apiUrl removed
|
|
42
|
+
}, [loadUsers, refreshTrigger]);
|
|
47
43
|
const handleDelete = async (userId) => {
|
|
48
44
|
if (!window.confirm(t('UserList.DELETE_CONFIRM', 'Are you sure you want to delete this user?')))
|
|
49
45
|
return;
|
|
50
46
|
try {
|
|
51
|
-
// FIX: Removed apiUrl parameter.
|
|
52
47
|
await deleteUser(userId);
|
|
53
48
|
setUsers(prev => prev.filter(u => u.id !== userId));
|
|
54
49
|
}
|
|
@@ -58,7 +53,6 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
58
53
|
};
|
|
59
54
|
const handleChangeRole = async (userId, newRole) => {
|
|
60
55
|
try {
|
|
61
|
-
// FIX: Removed apiUrl parameter.
|
|
62
56
|
await updateUserRole(userId, newRole);
|
|
63
57
|
await loadUsers();
|
|
64
58
|
}
|
|
@@ -68,7 +62,6 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
68
62
|
};
|
|
69
63
|
const handleToggleSupporter = async (userId, newValue) => {
|
|
70
64
|
try {
|
|
71
|
-
// FIX: Removed apiUrl parameter.
|
|
72
65
|
await updateUserSupportStatus(userId, newValue);
|
|
73
66
|
await loadUsers();
|
|
74
67
|
}
|
|
@@ -105,7 +98,7 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
105
98
|
extraContext,
|
|
106
99
|
t,
|
|
107
100
|
reloadUsers: loadUsers,
|
|
108
|
-
}), [currentUser, extraContext, t]);
|
|
101
|
+
}), [currentUser, extraContext, t, loadUsers]);
|
|
109
102
|
const visibleExtraColumns = useMemo(() => extraColumns.filter((column) => typeof column.visible === 'function' ? column.visible(listContext) : true), [extraColumns, listContext]);
|
|
110
103
|
const visibleRowActions = useMemo(() => extraRowActions.filter((action) => typeof action.visible === 'function' ? action.visible(listContext) : true), [extraRowActions, listContext]);
|
|
111
104
|
const getUserDisplayName = (user) => {
|
|
@@ -139,49 +132,6 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
139
132
|
}
|
|
140
133
|
return getExtraColumnSortValue(column, user);
|
|
141
134
|
};
|
|
142
|
-
const normalizeSortValue = (value) => {
|
|
143
|
-
if (value === null || value === undefined)
|
|
144
|
-
return '';
|
|
145
|
-
if (typeof value === 'boolean')
|
|
146
|
-
return value ? 1 : 0;
|
|
147
|
-
if (typeof value === 'number')
|
|
148
|
-
return value;
|
|
149
|
-
if (value instanceof Date)
|
|
150
|
-
return value.getTime();
|
|
151
|
-
return String(value).toLocaleLowerCase();
|
|
152
|
-
};
|
|
153
|
-
const compareSortValues = (a, b) => {
|
|
154
|
-
const av = normalizeSortValue(a);
|
|
155
|
-
const bv = normalizeSortValue(b);
|
|
156
|
-
if (typeof av === 'number' && typeof bv === 'number') {
|
|
157
|
-
return av - bv;
|
|
158
|
-
}
|
|
159
|
-
return String(av).localeCompare(String(bv), undefined, {
|
|
160
|
-
numeric: true,
|
|
161
|
-
sensitivity: 'base',
|
|
162
|
-
});
|
|
163
|
-
};
|
|
164
|
-
const getSortValueByKey = (user, sortKey) => {
|
|
165
|
-
var _a, _b;
|
|
166
|
-
if (sortKey === 'id')
|
|
167
|
-
return Number((_a = user === null || user === void 0 ? void 0 : user.id) !== null && _a !== void 0 ? _a : 0);
|
|
168
|
-
if (sortKey === 'email')
|
|
169
|
-
return (user === null || user === void 0 ? void 0 : user.email) || '';
|
|
170
|
-
if (sortKey === 'name')
|
|
171
|
-
return getUserDisplayName(user);
|
|
172
|
-
if (sortKey === 'role')
|
|
173
|
-
return (user === null || user === void 0 ? void 0 : user.role) || '';
|
|
174
|
-
if (sortKey === 'is_support_agent')
|
|
175
|
-
return Boolean(user === null || user === void 0 ? void 0 : user.is_support_agent);
|
|
176
|
-
if (sortKey.startsWith('extra:')) {
|
|
177
|
-
const extraKey = sortKey.slice('extra:'.length);
|
|
178
|
-
const column = visibleExtraColumns.find((col) => String(col.key) === extraKey);
|
|
179
|
-
if (!column)
|
|
180
|
-
return '';
|
|
181
|
-
return getExtraColumnSortValue(column, user);
|
|
182
|
-
}
|
|
183
|
-
return (_b = user === null || user === void 0 ? void 0 : user[sortKey]) !== null && _b !== void 0 ? _b : '';
|
|
184
|
-
};
|
|
185
135
|
const toSearchText = (value) => {
|
|
186
136
|
if (value === null || value === undefined)
|
|
187
137
|
return '';
|
|
@@ -207,36 +157,14 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
207
157
|
return [...baseValues, ...extraValues].map((value) => toSearchText(value)).filter(Boolean);
|
|
208
158
|
};
|
|
209
159
|
const normalizedSearch = searchQuery.trim().toLocaleLowerCase();
|
|
210
|
-
const
|
|
211
|
-
|
|
160
|
+
const filteredUsers = useMemo(() => {
|
|
161
|
+
return users.filter((user) => {
|
|
212
162
|
if (!normalizedSearch)
|
|
213
163
|
return true;
|
|
214
164
|
const bucket = getSearchBucketForUser(user);
|
|
215
165
|
return bucket.some((entry) => entry.includes(normalizedSearch));
|
|
216
166
|
});
|
|
217
|
-
|
|
218
|
-
var _a, _b;
|
|
219
|
-
const aValue = getSortValueByKey(a, sortConfig.key);
|
|
220
|
-
const bValue = getSortValueByKey(b, sortConfig.key);
|
|
221
|
-
const cmp = compareSortValues(aValue, bValue);
|
|
222
|
-
if (cmp !== 0)
|
|
223
|
-
return sortConfig.direction === 'asc' ? cmp : -cmp;
|
|
224
|
-
// Stable fallback
|
|
225
|
-
return Number((_a = a === null || a === void 0 ? void 0 : a.id) !== null && _a !== void 0 ? _a : 0) - Number((_b = b === null || b === void 0 ? void 0 : b.id) !== null && _b !== void 0 ? _b : 0);
|
|
226
|
-
});
|
|
227
|
-
return sorted;
|
|
228
|
-
}, [users, normalizedSearch, sortConfig, visibleExtraColumns, currentUser, extraContext, t]);
|
|
229
|
-
const handleSort = (columnKey) => {
|
|
230
|
-
setSortConfig((prev) => {
|
|
231
|
-
if (prev.key === columnKey) {
|
|
232
|
-
return {
|
|
233
|
-
key: columnKey,
|
|
234
|
-
direction: prev.direction === 'asc' ? 'desc' : 'asc',
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
return { key: columnKey, direction: 'asc' };
|
|
238
|
-
});
|
|
239
|
-
};
|
|
167
|
+
}, [users, normalizedSearch, visibleExtraColumns, currentUser, extraContext, t]);
|
|
240
168
|
const runRowAction = async (action, user) => {
|
|
241
169
|
const actionId = `${action.key}:${user.id}`;
|
|
242
170
|
setRowActionLoading((prev) => (Object.assign(Object.assign({}, prev), { [actionId]: true })));
|
|
@@ -258,27 +186,124 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
258
186
|
setRowActionLoading((prev) => (Object.assign(Object.assign({}, prev), { [actionId]: false })));
|
|
259
187
|
}
|
|
260
188
|
};
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
189
|
+
const columns = useMemo(() => {
|
|
190
|
+
const baseColumns = [
|
|
191
|
+
{
|
|
192
|
+
field: 'email',
|
|
193
|
+
headerName: t('Auth.EMAIL_LABEL', 'Email'),
|
|
194
|
+
minWidth: 240,
|
|
195
|
+
flex: 1.2,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
field: 'name',
|
|
199
|
+
headerName: t('Profile.NAME_LABEL', 'Name'),
|
|
200
|
+
minWidth: 220,
|
|
201
|
+
flex: 1,
|
|
202
|
+
valueGetter: (_value, row) => getUserDisplayName(row),
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
field: 'role',
|
|
206
|
+
headerName: t('UserList.ROLE', 'Role'),
|
|
207
|
+
minWidth: 180,
|
|
208
|
+
flex: 0.8,
|
|
209
|
+
valueGetter: (_value, row) => (row === null || row === void 0 ? void 0 : row.role) || 'none',
|
|
210
|
+
renderCell: (params) => {
|
|
211
|
+
const user = params.row;
|
|
212
|
+
return (_jsx(FormControl, { size: "small", fullWidth: true, sx: controlSx, disabled: !canEdit(user), children: _jsx(Select, { value: user.role || 'none', onChange: (event) => handleChangeRole(user.id, event.target.value), variant: "outlined", children: roles.map((role) => _jsx(MenuItem, { value: role, children: role }, role)) }) }));
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
field: 'is_support_agent',
|
|
217
|
+
headerName: t('UserList.SUPPORTER', 'Support Agent'),
|
|
218
|
+
minWidth: 190,
|
|
219
|
+
flex: 0.8,
|
|
220
|
+
valueGetter: (_value, row) => Boolean(row === null || row === void 0 ? void 0 : row.is_support_agent),
|
|
221
|
+
renderCell: (params) => {
|
|
222
|
+
const user = params.row;
|
|
223
|
+
return (_jsx(FormControl, { size: "small", fullWidth: true, sx: controlSx, disabled: !canEdit(user), children: _jsxs(Select, { value: user.is_support_agent ? 'yes' : 'no', onChange: (event) => handleToggleSupporter(user.id, event.target.value === 'yes'), variant: "outlined", children: [_jsx(MenuItem, { value: "yes", children: t('Common.YES', 'Yes') }), _jsx(MenuItem, { value: "no", children: t('Common.NO', 'No') })] }) }));
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
];
|
|
227
|
+
const mappedExtraColumns = visibleExtraColumns.map((column) => ({
|
|
228
|
+
field: `extra:${column.key}`,
|
|
229
|
+
headerName: typeof column.label === 'function' ? column.label(listContext) : column.label,
|
|
230
|
+
minWidth: Number(column.minWidth) || 180,
|
|
231
|
+
flex: Number(column.flex) || 0.9,
|
|
232
|
+
sortable: column.sortable !== false,
|
|
233
|
+
align: column.align || 'left',
|
|
234
|
+
headerAlign: column.align || 'left',
|
|
235
|
+
valueGetter: (_value, row) => getExtraColumnSortValue(column, row),
|
|
236
|
+
renderCell: (params) => column.renderCell({
|
|
237
|
+
user: params.row,
|
|
238
|
+
canEdit: canEdit(params.row),
|
|
239
|
+
currentUser,
|
|
240
|
+
extraContext,
|
|
241
|
+
t,
|
|
242
|
+
reloadUsers: loadUsers,
|
|
243
|
+
}),
|
|
244
|
+
}));
|
|
245
|
+
const actionColumn = {
|
|
246
|
+
field: 'actions',
|
|
247
|
+
headerName: t('Common.ACTIONS', 'Actions'),
|
|
248
|
+
minWidth: Math.max(220, 110 + visibleRowActions.length * 110),
|
|
249
|
+
flex: 1.4,
|
|
250
|
+
sortable: false,
|
|
251
|
+
filterable: false,
|
|
252
|
+
disableColumnMenu: true,
|
|
253
|
+
renderCell: (params) => {
|
|
254
|
+
const user = params.row;
|
|
255
|
+
return (_jsxs(Box, { sx: { display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center', py: 0.5 }, children: [visibleRowActions.map((action) => {
|
|
256
|
+
const actionId = `${action.key}:${user.id}`;
|
|
257
|
+
const isBusy = Boolean(rowActionLoading[actionId]);
|
|
258
|
+
const isDisabled = typeof action.disabled === 'function'
|
|
259
|
+
? action.disabled({
|
|
260
|
+
user,
|
|
261
|
+
canEdit: canEdit(user),
|
|
262
|
+
currentUser,
|
|
263
|
+
extraContext,
|
|
264
|
+
t,
|
|
265
|
+
})
|
|
266
|
+
: false;
|
|
267
|
+
return (_jsx(Button, { size: "small", variant: "outlined", onClick: () => runRowAction(action, user), disabled: isBusy || isDisabled, sx: actionButtonSx, children: typeof action.label === 'function'
|
|
268
|
+
? action.label({ user, t, currentUser, canEdit: canEdit(user) })
|
|
269
|
+
: action.label }, `${action.key}-${user.id}`));
|
|
270
|
+
}), _jsx(Tooltip, { title: t('Common.DELETE', 'Delete'), children: _jsx("span", { children: _jsx(Button, { size: "small", variant: "outlined", color: "error", startIcon: _jsx(DeleteIcon, {}), onClick: () => handleDelete(user.id), disabled: !canEdit(user), sx: actionButtonSx, children: t('Common.DELETE', 'Delete') }) }) })] }));
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
return [...baseColumns, ...mappedExtraColumns, actionColumn];
|
|
274
|
+
}, [
|
|
275
|
+
t,
|
|
276
|
+
roles,
|
|
277
|
+
visibleExtraColumns,
|
|
278
|
+
visibleRowActions,
|
|
279
|
+
listContext,
|
|
280
|
+
rowActionLoading,
|
|
281
|
+
currentUser,
|
|
282
|
+
extraContext,
|
|
283
|
+
loadUsers,
|
|
284
|
+
canEdit,
|
|
285
|
+
]);
|
|
286
|
+
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('UserList.TITLE', 'All Users') }), _jsx(Box, { sx: { mb: 2, maxWidth: 420 }, children: _jsx(TextField, { fullWidth: true, size: "small", label: t('Common.SEARCH', 'Search'), placeholder: t('UserList.SEARCH_PLACEHOLDER', 'Search users...'), value: searchQuery, onChange: (event) => setSearchQuery(event.target.value) }) }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(error) }), loading && (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', p: 3 }, children: _jsx(CircularProgress, {}) })), !loading && (_jsx(Box, { sx: { width: '100%', minHeight: 520 }, children: _jsx(DataGrid, { rows: filteredUsers, columns: columns, disableRowSelectionOnClick: true, showToolbar: true, getRowHeight: () => 'auto', pageSizeOptions: [10, 25, 50, 100], initialState: {
|
|
287
|
+
sorting: { sortModel: [{ field: 'email', sort: 'asc' }] },
|
|
288
|
+
pagination: { paginationModel: { pageSize: 25, page: 0 } },
|
|
289
|
+
}, localeText: {
|
|
290
|
+
noRowsLabel: t('UserList.NO_USERS', 'No users found.'),
|
|
291
|
+
toolbarQuickFilterPlaceholder: t('UserList.SEARCH_PLACEHOLDER', 'Search users...'),
|
|
292
|
+
}, slotProps: {
|
|
293
|
+
toolbar: {
|
|
294
|
+
showQuickFilter: true,
|
|
295
|
+
quickFilterProps: { debounceMs: 300 },
|
|
296
|
+
},
|
|
297
|
+
}, sx: {
|
|
298
|
+
border: 1,
|
|
299
|
+
borderColor: 'divider',
|
|
300
|
+
'& .MuiDataGrid-cell': {
|
|
301
|
+
display: 'flex',
|
|
302
|
+
alignItems: 'flex-start',
|
|
303
|
+
py: 1,
|
|
304
|
+
},
|
|
305
|
+
'& .MuiDataGrid-columnHeaders': {
|
|
306
|
+
bgcolor: 'action.hover',
|
|
307
|
+
},
|
|
308
|
+
} }) }))] }));
|
|
284
309
|
}
|
package/dist/index.js
CHANGED
|
@@ -22,5 +22,8 @@ export { AccessCodeManager } from './components/AccessCodeManager';
|
|
|
22
22
|
export { UserListComponent } from './components/UserListComponent';
|
|
23
23
|
export { UserInviteComponent } from './components/UserInviteComponent';
|
|
24
24
|
export { BulkInviteCsvTab } from './components/BulkInviteCsvTab';
|
|
25
|
+
export { RegistrationMethodsManager } from './components/RegistrationMethodsManager';
|
|
26
|
+
export { AuthFactorRequirementCard } from './components/AuthFactorRequirementCard';
|
|
27
|
+
export { QrSignupManager } from './components/QrSignupManager';
|
|
25
28
|
// --- 6. Translations ---
|
|
26
29
|
export { authTranslations } from './i18n/authTranslations';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { useContext, useMemo } from 'react';
|
|
2
|
+
import React, { useContext, useMemo, useState } from 'react';
|
|
3
3
|
import { Helmet } from 'react-helmet';
|
|
4
4
|
import { useSearchParams } from 'react-router-dom';
|
|
5
5
|
import { Tabs, Tab, Box, Typography, Alert, CircularProgress, Paper, Stack, } from '@mui/material';
|
|
@@ -13,6 +13,9 @@ import { SecurityComponent } from '../components/SecurityComponent';
|
|
|
13
13
|
import { UserListComponent } from '../components/UserListComponent';
|
|
14
14
|
import { UserInviteComponent } from '../components/UserInviteComponent';
|
|
15
15
|
import { AccessCodeManager } from '../components/AccessCodeManager';
|
|
16
|
+
import { RegistrationMethodsManager } from '../components/RegistrationMethodsManager';
|
|
17
|
+
import { AuthFactorRequirementCard } from '../components/AuthFactorRequirementCard';
|
|
18
|
+
import { QrSignupManager } from '../components/QrSignupManager';
|
|
16
19
|
import { SupportRecoveryRequestsTab } from '../components/SupportRecoveryRequestsTab';
|
|
17
20
|
import { BulkInviteCsvTab } from '../components/BulkInviteCsvTab';
|
|
18
21
|
import { updateUserProfile } from '../auth/authApi'; // Ggf. Pfad anpassen
|
|
@@ -21,6 +24,7 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
21
24
|
const { t } = useTranslation();
|
|
22
25
|
const { user, login, loading } = useContext(AuthContext);
|
|
23
26
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
27
|
+
const [authPolicy, setAuthPolicy] = useState(null);
|
|
24
28
|
// 1. URL State Management
|
|
25
29
|
const currentTabRaw = searchParams.get('tab') || 'profile';
|
|
26
30
|
const currentTab = ['invite', 'bulk-invite-csv', 'access'].includes(currentTabRaw)
|
|
@@ -87,5 +91,5 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
87
91
|
const activeExtraTab = builtInTabValues.has(safeTab)
|
|
88
92
|
? null
|
|
89
93
|
: extraTabs.find((tab) => tab.value === safeTab);
|
|
90
|
-
return (_jsxs(WidePage, { title: t('Account.TITLE', 'Account & Administration'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('Account.PAGE_TITLE', 'Account'), " \u2013 ", user.email] }) }), _jsx(Tabs, { value: safeTab, onChange: handleTabChange, variant: "scrollable", scrollButtons: "auto", sx: { mb: 3, borderBottom: 1, borderColor: 'divider' }, children: tabs.map((tab) => (_jsx(Tab, { label: tab.label, value: tab.value }, tab.value))) }), safeTab === 'profile' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(ProfileComponent, { onSubmit: handleProfileSubmit, showName: true, showPrivacy: true, showCookies: true }) })), safeTab === 'security' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SecurityComponent, { fromRecovery: fromRecovery, fromWeakLogin: fromWeakLogin }) })), safeTab === 'users' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(UserListComponent, { roles: activeRoles, currentUser: user, extraColumns: userListExtraColumns, extraRowActions: userListExtraRowActions, extraContext: userListExtraContext, refreshTrigger: userListRefreshTrigger, canEditUser: userListCanEditUser }) })), safeTab === 'invite' && (_jsx(Box, { sx: { mt: 2 }, children: _jsxs(Stack, { spacing: 2.5, children: [(isSuperUser || perms.can_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(UserInviteComponent, {}) })), (isSuperUser || perms.can_manage_access_codes) && (_jsxs(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: [_jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Account.ACCESS_CODES_HINT', 'Manage access codes for self-registration.') }), _jsx(AccessCodeManager, {})] })), (isSuperUser || perms.can_invite) && showBulkInviteCsvTab && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(BulkInviteCsvTab, Object.assign({}, bulkInviteCsvProps)) }))] }) })), safeTab === 'support' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SupportRecoveryRequestsTab, {}) })), activeExtraTab && (_jsx(Box, { sx: { mt: 2 }, children: (_a = activeExtraTab.render) === null || _a === void 0 ? void 0 : _a.call(activeExtraTab, { user, perms, isSuperUser, t }) }))] }));
|
|
94
|
+
return (_jsxs(WidePage, { title: t('Account.TITLE', 'Account & Administration'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('Account.PAGE_TITLE', 'Account'), " \u2013 ", user.email] }) }), _jsx(Tabs, { value: safeTab, onChange: handleTabChange, variant: "scrollable", scrollButtons: "auto", sx: { mb: 3, borderBottom: 1, borderColor: 'divider' }, children: tabs.map((tab) => (_jsx(Tab, { label: tab.label, value: tab.value }, tab.value))) }), safeTab === 'profile' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(ProfileComponent, { onSubmit: handleProfileSubmit, showName: true, showPrivacy: true, showCookies: true }) })), safeTab === 'security' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SecurityComponent, { fromRecovery: fromRecovery, fromWeakLogin: fromWeakLogin }) })), safeTab === 'users' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(UserListComponent, { roles: activeRoles, currentUser: user, extraColumns: userListExtraColumns, extraRowActions: userListExtraRowActions, extraContext: userListExtraContext, refreshTrigger: userListRefreshTrigger, canEditUser: userListCanEditUser }) })), safeTab === 'invite' && (_jsx(Box, { sx: { mt: 2 }, children: _jsxs(Stack, { spacing: 2.5, children: [(isSuperUser || perms.can_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(RegistrationMethodsManager, { onPolicyChange: setAuthPolicy }) })), (isSuperUser || perms.can_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(AuthFactorRequirementCard, {}) })), (isSuperUser || perms.can_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(UserInviteComponent, {}) })), (isSuperUser || perms.can_manage_access_codes) && (_jsxs(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: [_jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Account.ACCESS_CODES_HINT', 'Manage access codes for self-registration.') }), _jsx(AccessCodeManager, {})] })), (isSuperUser || perms.can_invite) && showBulkInviteCsvTab && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(BulkInviteCsvTab, Object.assign({}, bulkInviteCsvProps)) })), (isSuperUser || perms.can_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(QrSignupManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) }) }))] }) })), safeTab === 'support' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SupportRecoveryRequestsTab, {}) })), activeExtraTab && (_jsx(Box, { sx: { mt: 2 }, children: (_a = activeExtraTab.render) === null || _a === void 0 ? void 0 : _a.call(activeExtraTab, { user, perms, isSuperUser, t }) }))] }));
|
|
91
95
|
}
|
package/dist/pages/LoginPage.js
CHANGED
|
@@ -32,6 +32,21 @@ export function LoginPage() {
|
|
|
32
32
|
: recoveryTokenRaw;
|
|
33
33
|
// Backward-compatible fallback for legacy links using query parameters.
|
|
34
34
|
const recoveryEmail = hashParams.get('email') || params.get('email') || '';
|
|
35
|
+
const requestedNext = params.get('next');
|
|
36
|
+
const getRedirectTarget = (currentUser, options = {}) => {
|
|
37
|
+
var _a;
|
|
38
|
+
if (options.forceSecurityRedirect) {
|
|
39
|
+
return options.forceSecurityRedirect;
|
|
40
|
+
}
|
|
41
|
+
const requiresExtra = ((_a = currentUser === null || currentUser === void 0 ? void 0 : currentUser.security_state) === null || _a === void 0 ? void 0 : _a.requires_additional_security) === true;
|
|
42
|
+
if (requiresExtra) {
|
|
43
|
+
return '/account?tab=security&from=weak_login';
|
|
44
|
+
}
|
|
45
|
+
if (requestedNext && requestedNext.startsWith('/')) {
|
|
46
|
+
return requestedNext;
|
|
47
|
+
}
|
|
48
|
+
return '/';
|
|
49
|
+
};
|
|
35
50
|
useEffect(() => {
|
|
36
51
|
const socialError = params.get('error') || params.get('social');
|
|
37
52
|
if (socialError) {
|
|
@@ -39,30 +54,14 @@ export function LoginPage() {
|
|
|
39
54
|
}
|
|
40
55
|
}, [location.search]);
|
|
41
56
|
useEffect(() => {
|
|
42
|
-
var _a;
|
|
43
57
|
if (loading || !user)
|
|
44
58
|
return;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
navigate('/account?tab=security&from=weak_login', { replace: true });
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
navigate('/', { replace: true });
|
|
51
|
-
}
|
|
52
|
-
}, [loading, user, navigate]);
|
|
59
|
+
navigate(getRedirectTarget(user), { replace: true });
|
|
60
|
+
}, [loading, user, navigate, requestedNext]);
|
|
53
61
|
// --- Helper: Central Success Logic ---
|
|
54
62
|
const handleLoginSuccess = (user) => {
|
|
55
|
-
var _a;
|
|
56
63
|
login(user); // Update Context
|
|
57
|
-
|
|
58
|
-
const requiresExtra = ((_a = user.security_state) === null || _a === void 0 ? void 0 : _a.requires_additional_security) === true;
|
|
59
|
-
if (requiresExtra) {
|
|
60
|
-
navigate('/account?tab=security&from=weak_login');
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
// Standard Redirect (könnte man noch mit ?next=... erweitern)
|
|
64
|
-
navigate('/');
|
|
65
|
-
}
|
|
64
|
+
navigate(getRedirectTarget(user));
|
|
66
65
|
};
|
|
67
66
|
// --- Handlers ---
|
|
68
67
|
const handleSubmitCredentials = async ({ identifier, password }) => {
|
|
@@ -72,9 +71,10 @@ export function LoginPage() {
|
|
|
72
71
|
// A) Recovery Flow
|
|
73
72
|
if (recoveryToken) {
|
|
74
73
|
const result = await loginWithRecoveryPassword(identifier, password, recoveryToken);
|
|
75
|
-
// Recovery login implies a specific redirect usually, usually straight to security settings
|
|
76
74
|
login(result.user);
|
|
77
|
-
navigate(
|
|
75
|
+
navigate(getRedirectTarget(result.user, {
|
|
76
|
+
forceSecurityRedirect: '/account?tab=security&from=recovery',
|
|
77
|
+
}));
|
|
78
78
|
return;
|
|
79
79
|
}
|
|
80
80
|
// B) Standard Password Login
|
|
@@ -118,9 +118,10 @@ export function LoginPage() {
|
|
|
118
118
|
const handleMfaSuccess = ({ user, method }) => {
|
|
119
119
|
// MFA component should return the user object after verifying code
|
|
120
120
|
if (method === 'recovery_code') {
|
|
121
|
-
// Recovery codes often trigger a security check prompt
|
|
122
121
|
login(user);
|
|
123
|
-
navigate(
|
|
122
|
+
navigate(getRedirectTarget(user, {
|
|
123
|
+
forceSecurityRedirect: '/account?tab=security&from=recovery',
|
|
124
|
+
}));
|
|
124
125
|
}
|
|
125
126
|
else {
|
|
126
127
|
handleLoginSuccess(user);
|
|
@@ -137,8 +138,13 @@ export function LoginPage() {
|
|
|
137
138
|
const passwordLoginEnabled = Boolean(authMethods === null || authMethods === void 0 ? void 0 : authMethods.password_login) || Boolean(recoveryToken);
|
|
138
139
|
const socialLoginEnabled = Boolean(authMethods === null || authMethods === void 0 ? void 0 : authMethods.social_login) && socialProviders.length > 0;
|
|
139
140
|
const passkeyLoginEnabled = Boolean(authMethods === null || authMethods === void 0 ? void 0 : authMethods.passkey_login);
|
|
140
|
-
const
|
|
141
|
+
const signupModes = Array.isArray(authMethods === null || authMethods === void 0 ? void 0 : authMethods.signup_modes)
|
|
142
|
+
? authMethods.signup_modes.filter(Boolean)
|
|
143
|
+
: [];
|
|
144
|
+
const signupEnabled = signupModes.length > 0 || Boolean(authMethods === null || authMethods === void 0 ? void 0 : authMethods.signup);
|
|
141
145
|
const passwordResetEnabled = Boolean(authMethods === null || authMethods === void 0 ? void 0 : authMethods.password_reset);
|
|
146
|
+
const twoFactorRequired = Boolean(authMethods === null || authMethods === void 0 ? void 0 : authMethods.two_factor_required)
|
|
147
|
+
|| Number((authMethods === null || authMethods === void 0 ? void 0 : authMethods.required_auth_factor_count) || 1) >= 2;
|
|
142
148
|
// --- Render ---
|
|
143
|
-
return (_jsxs(NarrowPage, { title: t('Auth.PAGE_LOGIN_TITLE'), subtitle: t('Auth.PAGE_LOGIN_SUBTITLE'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t('Auth.PAGE_LOGIN_TITLE')] }) }), errorKey && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(errorKey) })), recoveryToken && !errorKey && (_jsx(Alert, { severity: "info", sx: { mb: 2 }, children: t('Auth.RECOVERY_LOGIN_WARNING', 'Recovery link validated. Please enter your password.') })), step === 'credentials' && (_jsx(LoginForm, { onSubmit: passwordLoginEnabled ? handleSubmitCredentials : null, onForgotPassword: passwordResetEnabled ? () => navigate('/reset-request-password') : null, onSocialLogin: socialLoginEnabled ? (provider) => startSocialLogin(provider) : null, socialProviders: socialProviders, onPasskeyLogin: passkeyLoginEnabled ? handlePasskeyLoginInitial : null, onSignUp: signupEnabled ? () => navigate('/signup') : null, disabled: submitting, initialIdentifier: recoveryEmail })), step === 'mfa' && mfaState && (_jsx(Box, { children: _jsx(MfaLoginComponent, { availableTypes: mfaState.availableTypes, identifier: mfaState.identifier, onSuccess: handleMfaSuccess, onCancel: handleMfaCancel }) }))] }));
|
|
149
|
+
return (_jsxs(NarrowPage, { title: t('Auth.PAGE_LOGIN_TITLE'), subtitle: t('Auth.PAGE_LOGIN_SUBTITLE'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t('Auth.PAGE_LOGIN_TITLE')] }) }), errorKey && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(errorKey) })), recoveryToken && !errorKey && (_jsx(Alert, { severity: "info", sx: { mb: 2 }, children: t('Auth.RECOVERY_LOGIN_WARNING', 'Recovery link validated. Please enter your password.') })), twoFactorRequired && !recoveryToken && (_jsx(Alert, { severity: "info", sx: { mb: 2 }, children: t('Auth.TWO_FACTOR_REQUIRED_HINT', 'This app requires two authentication factors for full access.') })), step === 'credentials' && (_jsx(LoginForm, { onSubmit: passwordLoginEnabled ? handleSubmitCredentials : null, onForgotPassword: passwordResetEnabled ? () => navigate('/reset-request-password') : null, onSocialLogin: socialLoginEnabled ? (provider) => startSocialLogin(provider) : null, socialProviders: socialProviders, onPasskeyLogin: passkeyLoginEnabled ? handlePasskeyLoginInitial : null, onSignUp: signupEnabled ? () => navigate('/signup') : null, disabled: submitting, initialIdentifier: recoveryEmail })), step === 'mfa' && mfaState && (_jsx(Box, { children: _jsx(MfaLoginComponent, { availableTypes: mfaState.availableTypes, identifier: mfaState.identifier, onSuccess: handleMfaSuccess, onCancel: handleMfaCancel }) }))] }));
|
|
144
150
|
}
|
|
@@ -13,6 +13,8 @@ export function PasswordInvitePage() {
|
|
|
13
13
|
const location = useLocation();
|
|
14
14
|
const navigate = useNavigate();
|
|
15
15
|
const { t } = useTranslation();
|
|
16
|
+
const searchParams = new URLSearchParams(location.search);
|
|
17
|
+
const nextPath = searchParams.get('next');
|
|
16
18
|
const [submitting, setSubmitting] = useState(false);
|
|
17
19
|
const [errorKey, setErrorKey] = useState(null);
|
|
18
20
|
const [successKey, setSuccessKey] = useState(null);
|
|
@@ -57,7 +59,10 @@ export function PasswordInvitePage() {
|
|
|
57
59
|
setSuccessKey(isInvite
|
|
58
60
|
? 'Auth.RESET_PASSWORD_SUCCESS_INVITE'
|
|
59
61
|
: 'Auth.RESET_PASSWORD_SUCCESS_RESET');
|
|
60
|
-
|
|
62
|
+
const target = nextPath
|
|
63
|
+
? `/login?next=${encodeURIComponent(nextPath)}`
|
|
64
|
+
: '/login';
|
|
65
|
+
navigate(target);
|
|
61
66
|
}
|
|
62
67
|
catch (err) {
|
|
63
68
|
setErrorKey(err.code || 'Auth.RESET_PASSWORD_FAILED');
|
package/dist/pages/SignUpPage.js
CHANGED
|
@@ -1,20 +1,79 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import { Box, TextField, Button, Typography, Alert, } from '@mui/material';
|
|
2
|
+
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { useNavigate, useLocation } from 'react-router-dom';
|
|
4
|
+
import { Alert, Box, Button, Stack, TextField, Typography, } from '@mui/material';
|
|
6
5
|
import { Helmet } from 'react-helmet';
|
|
7
6
|
import { useTranslation } from 'react-i18next';
|
|
8
7
|
import { NarrowPage } from '../layout/PageLayout';
|
|
9
|
-
import {
|
|
8
|
+
import { AuthContext } from '../auth/AuthContext';
|
|
9
|
+
import { submitRegistrationRequest } from '../auth/authApi';
|
|
10
|
+
const MODE_LABELS = {
|
|
11
|
+
self_signup_access_code: 'Auth.SIGNUP_ACCESS_CODE_TAB',
|
|
12
|
+
self_signup_open: 'Auth.SIGNUP_OPEN_TAB',
|
|
13
|
+
self_signup_email_domain: 'Auth.SIGNUP_EMAIL_DOMAIN_TAB',
|
|
14
|
+
self_signup_qr: 'Auth.SIGNUP_QR_TAB',
|
|
15
|
+
};
|
|
16
|
+
const MODE_HINTS = {
|
|
17
|
+
self_signup_access_code: 'Auth.SIGNUP_ACCESS_CODE_HINT',
|
|
18
|
+
self_signup_open: 'Auth.SIGNUP_OPEN_HINT',
|
|
19
|
+
self_signup_email_domain: 'Auth.SIGNUP_EMAIL_DOMAIN_HINT',
|
|
20
|
+
self_signup_qr: 'Auth.SIGNUP_QR_HINT',
|
|
21
|
+
};
|
|
10
22
|
export function SignUpPage() {
|
|
11
23
|
const navigate = useNavigate();
|
|
24
|
+
const location = useLocation();
|
|
12
25
|
const { t } = useTranslation();
|
|
26
|
+
const { authMethods } = useContext(AuthContext);
|
|
27
|
+
const signupModes = useMemo(() => {
|
|
28
|
+
const configured = Array.isArray(authMethods === null || authMethods === void 0 ? void 0 : authMethods.signup_modes)
|
|
29
|
+
? authMethods.signup_modes.filter(Boolean)
|
|
30
|
+
: [];
|
|
31
|
+
if (configured.length > 0) {
|
|
32
|
+
return configured;
|
|
33
|
+
}
|
|
34
|
+
return (authMethods === null || authMethods === void 0 ? void 0 : authMethods.signup) ? ['self_signup_access_code'] : [];
|
|
35
|
+
}, [authMethods]);
|
|
36
|
+
const query = new URLSearchParams(location.search);
|
|
37
|
+
const tokenFromUrl = query.get('rt') || '';
|
|
38
|
+
const initialMode = useMemo(() => {
|
|
39
|
+
if (tokenFromUrl && signupModes.includes('self_signup_qr')) {
|
|
40
|
+
return 'self_signup_qr';
|
|
41
|
+
}
|
|
42
|
+
return signupModes[0] || 'self_signup_access_code';
|
|
43
|
+
}, [signupModes, tokenFromUrl]);
|
|
44
|
+
const [mode, setMode] = useState(initialMode);
|
|
13
45
|
const [email, setEmail] = useState('');
|
|
14
46
|
const [accessCode, setAccessCode] = useState('');
|
|
15
47
|
const [submitting, setSubmitting] = useState(false);
|
|
16
48
|
const [successKey, setSuccessKey] = useState(null);
|
|
17
49
|
const [errorKey, setErrorKey] = useState(null);
|
|
50
|
+
const [qrHint, setQrHint] = useState('');
|
|
51
|
+
const modeHint = useMemo(() => {
|
|
52
|
+
if (mode === 'self_signup_access_code') {
|
|
53
|
+
return t(MODE_HINTS[mode], 'Use this option only if you were given an access code for signup.');
|
|
54
|
+
}
|
|
55
|
+
if (mode === 'self_signup_open') {
|
|
56
|
+
return t(MODE_HINTS[mode], 'Use this option for direct signup without an access code.');
|
|
57
|
+
}
|
|
58
|
+
if (mode === 'self_signup_email_domain') {
|
|
59
|
+
return t(MODE_HINTS[mode], 'Use an email address from an allowed domain for this signup flow.');
|
|
60
|
+
}
|
|
61
|
+
if (mode === 'self_signup_qr') {
|
|
62
|
+
return qrHint || t(MODE_HINTS[mode], 'Use a valid QR signup link to continue.');
|
|
63
|
+
}
|
|
64
|
+
return '';
|
|
65
|
+
}, [mode, qrHint, t]);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
setMode(initialMode);
|
|
68
|
+
}, [initialMode]);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!tokenFromUrl || mode !== 'self_signup_qr') {
|
|
71
|
+
setQrHint('');
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
setQrHint(t('Auth.SIGNUP_QR_READY', 'QR signup token detected. You can complete your signup now.'));
|
|
75
|
+
return undefined;
|
|
76
|
+
}, [mode, tokenFromUrl, t]);
|
|
18
77
|
const handleSubmit = async (event) => {
|
|
19
78
|
event.preventDefault();
|
|
20
79
|
setSuccessKey(null);
|
|
@@ -23,24 +82,25 @@ export function SignUpPage() {
|
|
|
23
82
|
setErrorKey('Auth.EMAIL_REQUIRED');
|
|
24
83
|
return;
|
|
25
84
|
}
|
|
26
|
-
if (!accessCode) {
|
|
85
|
+
if (mode === 'self_signup_access_code' && !accessCode) {
|
|
27
86
|
setErrorKey('Auth.SIGNUP_ACCESS_CODE_REQUIRED');
|
|
28
87
|
return;
|
|
29
88
|
}
|
|
89
|
+
if (mode === 'self_signup_qr' && !tokenFromUrl) {
|
|
90
|
+
setErrorKey('Auth.SIGNUP_QR_INVALID');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
30
93
|
setSubmitting(true);
|
|
31
94
|
try {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
// 2) Invite anfordern
|
|
39
|
-
await requestInviteWithCode(email, accessCode);
|
|
95
|
+
await submitRegistrationRequest({
|
|
96
|
+
email,
|
|
97
|
+
mode,
|
|
98
|
+
accessCode,
|
|
99
|
+
registrationContextToken: mode === 'self_signup_qr' ? tokenFromUrl : null,
|
|
100
|
+
});
|
|
40
101
|
setSuccessKey('Auth.INVITE_REQUEST_SUCCESS');
|
|
41
102
|
}
|
|
42
103
|
catch (err) {
|
|
43
|
-
// validateAccessCode / requestInviteWithCode liefern normalisierte Errors
|
|
44
104
|
setErrorKey(err.code || 'Auth.INVITE_FAILED');
|
|
45
105
|
}
|
|
46
106
|
finally {
|
|
@@ -50,7 +110,7 @@ export function SignUpPage() {
|
|
|
50
110
|
const handleGoToLogin = () => {
|
|
51
111
|
navigate('/login');
|
|
52
112
|
};
|
|
53
|
-
return (_jsxs(NarrowPage, { title: t('Auth.LOGIN_SIGNUP_BUTTON'), subtitle: t('Auth.PAGE_SIGNUP_SUBTITLE'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t('Auth.LOGIN_SIGNUP_BUTTON')] }) }), successKey && (_jsx(Alert, { severity: "success", sx: { mb: 2 }, children: t(successKey, { email }) })), errorKey && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(errorKey) })), _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: email, onChange: (e) => setEmail(e.target.value), disabled: submitting }), _jsx(TextField, { label: t('Auth.ACCESS_CODE_LABEL'), type: "text", required: true, fullWidth: true, value: accessCode, onChange: (e) => setAccessCode(e.target.value), disabled: submitting }), _jsx(Button, { type: "submit", variant: "contained", disabled: submitting, children: submitting
|
|
113
|
+
return (_jsxs(NarrowPage, { title: t('Auth.LOGIN_SIGNUP_BUTTON'), subtitle: t('Auth.PAGE_SIGNUP_SUBTITLE'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t('Auth.LOGIN_SIGNUP_BUTTON')] }) }), successKey && (_jsx(Alert, { severity: "success", sx: { mb: 2 }, children: t(successKey, { email }) })), errorKey && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(errorKey, t('Auth.INVITE_FAILED', 'Could not complete signup.')) })), signupModes.length > 1 && (_jsxs(Stack, { spacing: 1, sx: { mb: 2 }, children: [_jsx(Alert, { severity: "info", children: t('Auth.SIGNUP_MODE_SELECTOR_HINT', 'Choose the signup option that matches how you want to register.') }), _jsx(Stack, { direction: { xs: 'column', sm: 'row' }, spacing: 1, flexWrap: "wrap", children: signupModes.map((entry) => (_jsx(Button, { variant: mode === entry ? 'contained' : 'outlined', onClick: () => setMode(entry), disabled: submitting, children: t(MODE_LABELS[entry] || entry, entry) }, entry))) })] })), _jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { display: 'flex', flexDirection: 'column', gap: 2 }, children: [modeHint && _jsx(Alert, { severity: "info", children: modeHint }), _jsx(TextField, { label: t('Auth.EMAIL_LABEL'), type: "email", required: true, fullWidth: true, value: email, onChange: (e) => setEmail(e.target.value), disabled: submitting }), mode === 'self_signup_access_code' && (_jsx(TextField, { label: t('Auth.ACCESS_CODE_LABEL'), type: "text", required: true, fullWidth: true, value: accessCode, onChange: (e) => setAccessCode(e.target.value), disabled: submitting })), mode === 'self_signup_qr' && (_jsx(Stack, { spacing: 1, children: _jsx(TextField, { label: t('Auth.SIGNUP_QR_TOKEN_LABEL', 'QR token'), value: tokenFromUrl, fullWidth: true, InputProps: { readOnly: true } }) })), _jsx(Button, { type: "submit", variant: "contained", disabled: submitting || signupModes.length === 0, children: submitting
|
|
54
114
|
? t('Auth.SIGNUP_SUBMITTING')
|
|
55
115
|
: t('Auth.SIGNUP_SUBMIT') })] }), _jsx(Box, { sx: { mt: 3 }, children: _jsxs(Typography, { variant: "body2", children: [t('Auth.SIGNUP_ALREADY_HAVE_ACCOUNT'), ' ', _jsx(Button, { onClick: handleGoToLogin, variant: "text", size: "small", children: t('Auth.SIGNUP_GO_TO_LOGIN') })] }) })] }));
|
|
56
116
|
}
|