@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.
@@ -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, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, FormControl, Select, MenuItem, Button, Tooltip, CircularProgress, Alert, TableSortLabel, TextField } from '@mui/material';
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
- // eslint-disable-next-line react-hooks/exhaustive-deps
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 filteredAndSortedUsers = useMemo(() => {
211
- const filtered = users.filter((user) => {
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
- const sorted = [...filtered].sort((a, b) => {
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
- 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, {}) })) : (_jsx(TableContainer, { component: Paper, children: _jsxs(Table, { size: "small", children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { sx: cellSx, sortDirection: sortConfig.key === 'email' ? sortConfig.direction : false, children: _jsx(TableSortLabel, { active: sortConfig.key === 'email', direction: sortConfig.key === 'email' ? sortConfig.direction : 'asc', onClick: () => handleSort('email'), children: t('Auth.EMAIL_LABEL', 'Email') }) }), _jsx(TableCell, { sx: cellSx, sortDirection: sortConfig.key === 'name' ? sortConfig.direction : false, children: _jsx(TableSortLabel, { active: sortConfig.key === 'name', direction: sortConfig.key === 'name' ? sortConfig.direction : 'asc', onClick: () => handleSort('name'), children: t('Profile.NAME_LABEL', 'Name') }) }), _jsx(TableCell, { sx: cellSx, sortDirection: sortConfig.key === 'role' ? sortConfig.direction : false, children: _jsx(TableSortLabel, { active: sortConfig.key === 'role', direction: sortConfig.key === 'role' ? sortConfig.direction : 'asc', onClick: () => handleSort('role'), children: t('UserList.ROLE', 'Role') }) }), _jsx(TableCell, { sx: cellSx, sortDirection: sortConfig.key === 'is_support_agent' ? sortConfig.direction : false, children: _jsx(TableSortLabel, { active: sortConfig.key === 'is_support_agent', direction: sortConfig.key === 'is_support_agent' ? sortConfig.direction : 'asc', onClick: () => handleSort('is_support_agent'), children: t('UserList.SUPPORTER', 'Support Agent') }) }), visibleExtraColumns.map((column) => (_jsx(TableCell, { sx: cellSx, align: column.align || 'left', sortDirection: sortConfig.key === `extra:${column.key}` ? sortConfig.direction : false, children: _jsx(TableSortLabel, { active: sortConfig.key === `extra:${column.key}`, direction: sortConfig.key === `extra:${column.key}` ? sortConfig.direction : 'asc', onClick: () => handleSort(`extra:${column.key}`), children: typeof column.label === 'function' ? column.label(listContext) : column.label }) }, column.key))), _jsx(TableCell, { sx: cellSx, children: t('Common.ACTIONS', 'Actions') })] }) }), _jsxs(TableBody, { children: [filteredAndSortedUsers.map((u) => (_jsxs(TableRow, { children: [_jsx(TableCell, { sx: cellSx, children: u.email }), _jsx(TableCell, { sx: cellSx, children: getUserDisplayName(u) }), _jsx(TableCell, { sx: cellSx, children: _jsx(FormControl, { size: "small", fullWidth: true, sx: controlSx, disabled: !canEdit(u), children: _jsx(Select, { value: u.role || 'none', onChange: (e) => handleChangeRole(u.id, e.target.value), variant: "outlined", children: roles.map(r => _jsx(MenuItem, { value: r, children: r }, r)) }) }) }), _jsx(TableCell, { sx: cellSx, children: _jsx(FormControl, { size: "small", fullWidth: true, sx: controlSx, disabled: !canEdit(u), children: _jsxs(Select, { value: u.is_support_agent ? 'yes' : 'no', onChange: (e) => handleToggleSupporter(u.id, e.target.value === 'yes'), variant: "outlined", children: [_jsx(MenuItem, { value: "yes", children: t('Common.YES', 'Yes') }), _jsx(MenuItem, { value: "no", children: t('Common.NO', 'No') })] }) }) }), visibleExtraColumns.map((column) => (_jsx(TableCell, { sx: cellSx, align: column.align || 'left', children: column.renderCell({
262
- user: u,
263
- canEdit: canEdit(u),
264
- currentUser,
265
- extraContext,
266
- t,
267
- reloadUsers: loadUsers,
268
- }) }, `${column.key}-${u.id}`))), _jsx(TableCell, { sx: cellSx, children: _jsxs(Box, { sx: { display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center' }, children: [visibleRowActions.map((action) => {
269
- const actionId = `${action.key}:${u.id}`;
270
- const isBusy = Boolean(rowActionLoading[actionId]);
271
- const isDisabled = typeof action.disabled === 'function'
272
- ? action.disabled({
273
- user: u,
274
- canEdit: canEdit(u),
275
- currentUser,
276
- extraContext,
277
- t,
278
- })
279
- : false;
280
- return (_jsx(Button, { size: "small", variant: "outlined", onClick: () => runRowAction(action, u), disabled: isBusy || isDisabled, sx: actionButtonSx, children: typeof action.label === 'function'
281
- ? action.label({ user: u, t, currentUser, canEdit: canEdit(u) })
282
- : action.label }, `${action.key}-${u.id}`));
283
- }), _jsx(Tooltip, { title: t('Common.DELETE'), children: _jsx("span", { children: _jsx(Button, { size: "small", variant: "outlined", color: "error", startIcon: _jsx(DeleteIcon, {}), onClick: () => handleDelete(u.id), disabled: !canEdit(u), sx: actionButtonSx, children: t('Common.DELETE', 'Delete') }) }) })] }) })] }, u.id))), filteredAndSortedUsers.length === 0 && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5 + visibleExtraColumns.length, align: "center", children: t('UserList.NO_USERS', 'No users found.') }) }))] })] }) }))] }));
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
  }
@@ -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
- const requiresExtra = ((_a = user.security_state) === null || _a === void 0 ? void 0 : _a.requires_additional_security) === true;
46
- if (requiresExtra) {
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
- // Check if "Strong Security" is enforced/required but not met
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('/account?tab=security&from=recovery');
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('/account?tab=security&from=recovery');
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 signupEnabled = Boolean(authMethods === null || authMethods === void 0 ? void 0 : authMethods.signup);
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
- navigate('/login');
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');
@@ -1,20 +1,79 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- // src/pages/SignUpPage.jsx
3
- import React, { useState } from 'react';
4
- import { useNavigate } from 'react-router-dom';
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 { validateAccessCode, requestInviteWithCode } from '../auth/authApi';
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
- // 1) Access-Code prüfen
33
- const res = await validateAccessCode(accessCode);
34
- if (!(res === null || res === void 0 ? void 0 : res.valid)) {
35
- setErrorKey('Auth.ACCESS_CODE_INVALID_OR_INACTIVE');
36
- return;
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
  }