@micha.bigler/ui-core-micha 2.1.16 → 2.1.18
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 +34 -16
- package/dist/auth/authApi.js +19 -0
- package/dist/components/AccessCodeManager.js +8 -2
- package/dist/components/BulkInviteCsvTab.js +7 -1
- package/dist/components/LoginForm.js +2 -2
- package/dist/components/ProfileComponent.js +65 -5
- package/dist/components/SecurityComponent.js +23 -6
- package/dist/components/SocialLoginButtons.js +7 -4
- package/dist/components/UserInviteComponent.js +8 -2
- package/dist/components/UserListComponent.js +163 -21
- package/dist/i18n/authTranslations.js +30 -0
- package/dist/pages/AccountPage.js +11 -22
- package/dist/pages/LoginPage.js +16 -2
- package/dist/utils/authService.js +41 -2
- package/package.json +2 -1
- package/src/auth/AuthContext.jsx +64 -16
- package/src/auth/authApi.jsx +22 -1
- package/src/components/AccessCodeManager.jsx +14 -4
- package/src/components/BulkInviteCsvTab.jsx +9 -3
- package/src/components/LoginForm.jsx +58 -48
- package/src/components/ProfileComponent.jsx +117 -4
- package/src/components/SecurityComponent.jsx +58 -28
- package/src/components/SocialLoginButtons.jsx +57 -49
- package/src/components/UserInviteComponent.jsx +11 -4
- package/src/components/UserListComponent.jsx +252 -40
- package/src/i18n/authTranslations.ts +31 -1
- package/src/pages/AccountPage.jsx +34 -39
- package/src/pages/LoginPage.jsx +25 -6
- package/src/utils/authService.js +51 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React, { useMemo, useState, useEffect } from 'react';
|
|
3
|
-
import { Box, Typography, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, FormControl, Select, MenuItem, Button,
|
|
3
|
+
import { Box, Typography, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, FormControl, Select, MenuItem, Button, Tooltip, CircularProgress, Alert, TableSortLabel, TextField } from '@mui/material';
|
|
4
4
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import { fetchUsersList, deleteUser, updateUserRole, updateUserSupportStatus } from '../auth/authApi';
|
|
@@ -11,6 +11,11 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
11
11
|
const [loading, setLoading] = useState(true);
|
|
12
12
|
const [error, setError] = useState(null);
|
|
13
13
|
const [rowActionLoading, setRowActionLoading] = useState({});
|
|
14
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
15
|
+
const [sortConfig, setSortConfig] = useState({ key: 'id', direction: 'asc' });
|
|
16
|
+
const cellSx = { verticalAlign: 'middle' };
|
|
17
|
+
const controlSx = { minWidth: 140 };
|
|
18
|
+
const actionButtonSx = { textTransform: 'none', minWidth: 90 };
|
|
14
19
|
const loadUsers = async () => {
|
|
15
20
|
setLoading(true);
|
|
16
21
|
setError(null);
|
|
@@ -18,7 +23,15 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
18
23
|
// FIX: Removed apiUrl parameter.
|
|
19
24
|
// fetchUsersList uses USERS_BASE from authConfig internally.
|
|
20
25
|
const data = await fetchUsersList();
|
|
21
|
-
|
|
26
|
+
const list = Array.isArray(data) ? data : (Array.isArray(data === null || data === void 0 ? void 0 : data.results) ? data.results : []);
|
|
27
|
+
// Keep row identity stable even if backend returns unordered results.
|
|
28
|
+
list.sort((a, b) => {
|
|
29
|
+
var _a, _b;
|
|
30
|
+
const aId = Number((_a = a === null || a === void 0 ? void 0 : a.id) !== null && _a !== void 0 ? _a : 0);
|
|
31
|
+
const bId = Number((_b = b === null || b === void 0 ? void 0 : b.id) !== null && _b !== void 0 ? _b : 0);
|
|
32
|
+
return aId - bId;
|
|
33
|
+
});
|
|
34
|
+
setUsers(list);
|
|
22
35
|
}
|
|
23
36
|
catch (err) {
|
|
24
37
|
setError(err.code || 'Auth.USER_LIST_FAILED');
|
|
@@ -47,7 +60,7 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
47
60
|
try {
|
|
48
61
|
// FIX: Removed apiUrl parameter.
|
|
49
62
|
await updateUserRole(userId, newRole);
|
|
50
|
-
loadUsers();
|
|
63
|
+
await loadUsers();
|
|
51
64
|
}
|
|
52
65
|
catch (err) {
|
|
53
66
|
alert(t(err.code || 'Auth.USER_ROLE_UPDATE_FAILED'));
|
|
@@ -57,7 +70,7 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
57
70
|
try {
|
|
58
71
|
// FIX: Removed apiUrl parameter.
|
|
59
72
|
await updateUserSupportStatus(userId, newValue);
|
|
60
|
-
loadUsers();
|
|
73
|
+
await loadUsers();
|
|
61
74
|
}
|
|
62
75
|
catch (err) {
|
|
63
76
|
alert(t(err.code || 'Auth.USER_UPDATE_FAILED'));
|
|
@@ -95,6 +108,135 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
95
108
|
}), [currentUser, extraContext, t]);
|
|
96
109
|
const visibleExtraColumns = useMemo(() => extraColumns.filter((column) => typeof column.visible === 'function' ? column.visible(listContext) : true), [extraColumns, listContext]);
|
|
97
110
|
const visibleRowActions = useMemo(() => extraRowActions.filter((action) => typeof action.visible === 'function' ? action.visible(listContext) : true), [extraRowActions, listContext]);
|
|
111
|
+
const getUserDisplayName = (user) => {
|
|
112
|
+
if ((user === null || user === void 0 ? void 0 : user.first_name) || (user === null || user === void 0 ? void 0 : user.last_name)) {
|
|
113
|
+
return `${user.first_name || ''} ${user.last_name || ''}`.trim();
|
|
114
|
+
}
|
|
115
|
+
return (user === null || user === void 0 ? void 0 : user.username) || '';
|
|
116
|
+
};
|
|
117
|
+
const getExtraColumnSortValue = (column, user) => {
|
|
118
|
+
var _a;
|
|
119
|
+
const valueContext = { user, currentUser, extraContext, t };
|
|
120
|
+
if (typeof column.getSortValue === 'function') {
|
|
121
|
+
return column.getSortValue(valueContext);
|
|
122
|
+
}
|
|
123
|
+
if (typeof column.sortValue === 'function') {
|
|
124
|
+
return column.sortValue(valueContext);
|
|
125
|
+
}
|
|
126
|
+
if (typeof column.getSearchValue === 'function') {
|
|
127
|
+
return column.getSearchValue(valueContext);
|
|
128
|
+
}
|
|
129
|
+
if ((user === null || user === void 0 ? void 0 : user[column.key]) !== undefined)
|
|
130
|
+
return user[column.key];
|
|
131
|
+
if (((_a = user === null || user === void 0 ? void 0 : user.profile) === null || _a === void 0 ? void 0 : _a[column.key]) !== undefined)
|
|
132
|
+
return user.profile[column.key];
|
|
133
|
+
return '';
|
|
134
|
+
};
|
|
135
|
+
const getExtraColumnSearchValue = (column, user) => {
|
|
136
|
+
const valueContext = { user, currentUser, extraContext, t };
|
|
137
|
+
if (typeof column.getSearchValue === 'function') {
|
|
138
|
+
return column.getSearchValue(valueContext);
|
|
139
|
+
}
|
|
140
|
+
return getExtraColumnSortValue(column, user);
|
|
141
|
+
};
|
|
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
|
+
const toSearchText = (value) => {
|
|
186
|
+
if (value === null || value === undefined)
|
|
187
|
+
return '';
|
|
188
|
+
if (Array.isArray(value))
|
|
189
|
+
return value.map((item) => toSearchText(item)).join(' ');
|
|
190
|
+
if (typeof value === 'object')
|
|
191
|
+
return Object.values(value).map((item) => toSearchText(item)).join(' ');
|
|
192
|
+
return String(value).toLocaleLowerCase();
|
|
193
|
+
};
|
|
194
|
+
const getSearchBucketForUser = (user) => {
|
|
195
|
+
const baseValues = [
|
|
196
|
+
user === null || user === void 0 ? void 0 : user.id,
|
|
197
|
+
user === null || user === void 0 ? void 0 : user.email,
|
|
198
|
+
user === null || user === void 0 ? void 0 : user.username,
|
|
199
|
+
user === null || user === void 0 ? void 0 : user.first_name,
|
|
200
|
+
user === null || user === void 0 ? void 0 : user.last_name,
|
|
201
|
+
getUserDisplayName(user),
|
|
202
|
+
user === null || user === void 0 ? void 0 : user.role,
|
|
203
|
+
user === null || user === void 0 ? void 0 : user.language,
|
|
204
|
+
(user === null || user === void 0 ? void 0 : user.is_support_agent) ? t('Common.YES', 'Yes') : t('Common.NO', 'No'),
|
|
205
|
+
];
|
|
206
|
+
const extraValues = visibleExtraColumns.map((column) => getExtraColumnSearchValue(column, user));
|
|
207
|
+
return [...baseValues, ...extraValues].map((value) => toSearchText(value)).filter(Boolean);
|
|
208
|
+
};
|
|
209
|
+
const normalizedSearch = searchQuery.trim().toLocaleLowerCase();
|
|
210
|
+
const filteredAndSortedUsers = useMemo(() => {
|
|
211
|
+
const filtered = users.filter((user) => {
|
|
212
|
+
if (!normalizedSearch)
|
|
213
|
+
return true;
|
|
214
|
+
const bucket = getSearchBucketForUser(user);
|
|
215
|
+
return bucket.some((entry) => entry.includes(normalizedSearch));
|
|
216
|
+
});
|
|
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
|
+
};
|
|
98
240
|
const runRowAction = async (action, user) => {
|
|
99
241
|
const actionId = `${action.key}:${user.id}`;
|
|
100
242
|
setRowActionLoading((prev) => (Object.assign(Object.assign({}, prev), { [actionId]: true })));
|
|
@@ -116,27 +258,27 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
116
258
|
setRowActionLoading((prev) => (Object.assign(Object.assign({}, prev), { [actionId]: false })));
|
|
117
259
|
}
|
|
118
260
|
};
|
|
119
|
-
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('UserList.TITLE', 'All Users') }), 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, { children: t('Auth.EMAIL_LABEL', 'Email') }), _jsx(TableCell, { children: t('Profile.NAME_LABEL', 'Name') }), _jsx(TableCell, { children: t('UserList.ROLE', 'Role') }), _jsx(TableCell, { children: t('UserList.SUPPORTER', 'Support Agent') }), visibleExtraColumns.map((column) => (_jsx(TableCell, { align: column.align || 'left', children: typeof column.label === 'function' ? column.label(listContext) : column.label }, column.key))), _jsx(TableCell, { children: t('Common.ACTIONS', 'Actions') })] }) }), _jsxs(TableBody, { children: [
|
|
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({
|
|
120
262
|
user: u,
|
|
121
263
|
canEdit: canEdit(u),
|
|
122
264
|
currentUser,
|
|
123
265
|
extraContext,
|
|
124
266
|
t,
|
|
125
267
|
reloadUsers: loadUsers,
|
|
126
|
-
}) }, `${column.key}-${u.id}`))),
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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.') }) }))] })] }) }))] }));
|
|
142
284
|
}
|
|
@@ -810,6 +810,36 @@ export const authTranslations = {
|
|
|
810
810
|
"en": "I allow convenience cookies.",
|
|
811
811
|
"sw": "Ninaruhusu vidakuzi vya urahisi."
|
|
812
812
|
},
|
|
813
|
+
"Profile.PRIVACY_STATEMENT_TITLE": {
|
|
814
|
+
"de": "Datenschutzerklärung",
|
|
815
|
+
"fr": "Déclaration de confidentialité",
|
|
816
|
+
"en": "Privacy statement",
|
|
817
|
+
"sw": "Taarifa ya faragha"
|
|
818
|
+
},
|
|
819
|
+
"Profile.COOKIES_STATEMENT_TITLE": {
|
|
820
|
+
"de": "Cookie-Richtlinie",
|
|
821
|
+
"fr": "Politique relative aux cookies",
|
|
822
|
+
"en": "Cookie statement",
|
|
823
|
+
"sw": "Taarifa ya vidakuzi"
|
|
824
|
+
},
|
|
825
|
+
"Profile.STATEMENT_LOADING": {
|
|
826
|
+
"de": "Statement wird geladen...",
|
|
827
|
+
"fr": "Chargement du document...",
|
|
828
|
+
"en": "Loading statement...",
|
|
829
|
+
"sw": "Inapakia taarifa..."
|
|
830
|
+
},
|
|
831
|
+
"Profile.PRIVACY_STATEMENT_EMPTY": {
|
|
832
|
+
"de": "Die Datenschutzerklärung ist derzeit nicht verfügbar.",
|
|
833
|
+
"fr": "La déclaration de confidentialité n'est actuellement pas disponible.",
|
|
834
|
+
"en": "Privacy statement is currently unavailable.",
|
|
835
|
+
"sw": "Taarifa ya faragha haipatikani kwa sasa."
|
|
836
|
+
},
|
|
837
|
+
"Profile.COOKIES_STATEMENT_EMPTY": {
|
|
838
|
+
"de": "Die Cookie-Richtlinie ist derzeit nicht verfügbar.",
|
|
839
|
+
"fr": "La politique relative aux cookies n'est actuellement pas disponible.",
|
|
840
|
+
"en": "Cookie statement is currently unavailable.",
|
|
841
|
+
"sw": "Taarifa ya vidakuzi haipatikani kwa sasa."
|
|
842
|
+
},
|
|
813
843
|
"Profile.SAVE_BUTTON": {
|
|
814
844
|
"de": "Speichern",
|
|
815
845
|
"fr": "Enregistrer",
|
|
@@ -2,15 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import React, { useContext, useMemo } from 'react';
|
|
3
3
|
import { Helmet } from 'react-helmet';
|
|
4
4
|
import { useSearchParams } from 'react-router-dom';
|
|
5
|
-
import { Tabs, Tab, Box, Typography, Alert, CircularProgress } from '@mui/material';
|
|
5
|
+
import { Tabs, Tab, Box, Typography, Alert, CircularProgress, Paper, Stack, } from '@mui/material';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
|
-
// Internal
|
|
8
|
-
|
|
9
|
-
// Da Sie '@micha.bigler/ui-core-micha' nutzen, sollten die Imports ggf. so aussehen:
|
|
10
|
-
import { AuthContext,
|
|
11
|
-
// ... andere Komponenten aus der Lib importieren, falls sie dort exportiert sind
|
|
12
|
-
// Wenn sie lokal sind, lassen Sie die relativen Pfade.
|
|
13
|
-
} from '@micha.bigler/ui-core-micha';
|
|
7
|
+
// Internal context
|
|
8
|
+
import { AuthContext } from '../auth/AuthContext';
|
|
14
9
|
// Falls die Komponenten noch lokal sind:
|
|
15
10
|
import { WidePage } from '../layout/PageLayout';
|
|
16
11
|
import { ProfileComponent } from '../components/ProfileComponent';
|
|
@@ -27,7 +22,10 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
27
22
|
const { user, login, loading } = useContext(AuthContext);
|
|
28
23
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
29
24
|
// 1. URL State Management
|
|
30
|
-
const
|
|
25
|
+
const currentTabRaw = searchParams.get('tab') || 'profile';
|
|
26
|
+
const currentTab = ['invite', 'bulk-invite-csv', 'access'].includes(currentTabRaw)
|
|
27
|
+
? 'invite'
|
|
28
|
+
: currentTabRaw;
|
|
31
29
|
const fromRecovery = searchParams.get('from') === 'recovery';
|
|
32
30
|
const fromWeakLogin = searchParams.get('from') === 'weak_login';
|
|
33
31
|
// 2. Data & Permissions
|
|
@@ -55,17 +53,8 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
55
53
|
if (isSuperUser || perms.can_view_users) {
|
|
56
54
|
list.push({ value: 'users', label: t('Account.TAB_USERS', 'Users') });
|
|
57
55
|
}
|
|
58
|
-
if (isSuperUser || perms.can_invite) {
|
|
56
|
+
if (isSuperUser || perms.can_invite || perms.can_manage_access_codes) {
|
|
59
57
|
list.push({ value: 'invite', label: t('Account.TAB_INVITE', 'Invite') });
|
|
60
|
-
if (showBulkInviteCsvTab) {
|
|
61
|
-
list.push({
|
|
62
|
-
value: 'bulk-invite-csv',
|
|
63
|
-
label: t('Account.TAB_BULK_INVITE_CSV', 'Bulk Invite CSV'),
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
if (isSuperUser || perms.can_manage_access_codes) {
|
|
68
|
-
list.push({ value: 'access', label: t('Account.TAB_ACCESS_CODES', 'Access Codes') });
|
|
69
58
|
}
|
|
70
59
|
if (isSuperUser || perms.can_view_support) {
|
|
71
60
|
list.push({ value: 'support', label: t('Account.TAB_SUPPORT', 'Support') });
|
|
@@ -82,7 +71,7 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
82
71
|
}
|
|
83
72
|
});
|
|
84
73
|
return list;
|
|
85
|
-
}, [user, perms, t, isSuperUser,
|
|
74
|
+
}, [user, perms, t, isSuperUser, extraTabs]);
|
|
86
75
|
// 4. Loading & Auth Checks
|
|
87
76
|
if (loading) {
|
|
88
77
|
return (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', mt: 10 }, children: _jsx(CircularProgress, {}) }));
|
|
@@ -94,9 +83,9 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
94
83
|
const activeTabExists = tabs.some(t => t.value === currentTab);
|
|
95
84
|
// Falls der Tab nicht erlaubt ist (z.B. manuell in URL eingegeben), Fallback auf 'profile'
|
|
96
85
|
const safeTab = activeTabExists ? currentTab : 'profile';
|
|
97
|
-
const builtInTabValues = new Set(['profile', 'security', 'users', 'invite', '
|
|
86
|
+
const builtInTabValues = new Set(['profile', 'security', 'users', 'invite', 'support']);
|
|
98
87
|
const activeExtraTab = builtInTabValues.has(safeTab)
|
|
99
88
|
? null
|
|
100
89
|
: extraTabs.find((tab) => tab.value === safeTab);
|
|
101
|
-
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:
|
|
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 }) }))] }));
|
|
102
91
|
}
|
package/dist/pages/LoginPage.js
CHANGED
|
@@ -16,7 +16,7 @@ import { MfaLoginComponent } from '../components/MfaLoginComponent';
|
|
|
16
16
|
export function LoginPage() {
|
|
17
17
|
const navigate = useNavigate();
|
|
18
18
|
const location = useLocation();
|
|
19
|
-
const { login } = useContext(AuthContext);
|
|
19
|
+
const { login, authMethods } = useContext(AuthContext);
|
|
20
20
|
const { t } = useTranslation();
|
|
21
21
|
// State
|
|
22
22
|
const [step, setStep] = useState('credentials'); // 'credentials' | 'mfa'
|
|
@@ -32,6 +32,12 @@ 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
|
+
useEffect(() => {
|
|
36
|
+
const socialError = params.get('error') || params.get('social');
|
|
37
|
+
if (socialError) {
|
|
38
|
+
setErrorKey('Auth.SOCIAL_LOGIN_FAILED');
|
|
39
|
+
}
|
|
40
|
+
}, [location.search]);
|
|
35
41
|
// --- Helper: Central Success Logic ---
|
|
36
42
|
const handleLoginSuccess = (user) => {
|
|
37
43
|
var _a;
|
|
@@ -113,6 +119,14 @@ export function LoginPage() {
|
|
|
113
119
|
setMfaState(null);
|
|
114
120
|
setErrorKey(null);
|
|
115
121
|
};
|
|
122
|
+
const socialProviders = Array.isArray(authMethods === null || authMethods === void 0 ? void 0 : authMethods.social_providers)
|
|
123
|
+
? authMethods.social_providers
|
|
124
|
+
: [];
|
|
125
|
+
const passwordLoginEnabled = Boolean(authMethods === null || authMethods === void 0 ? void 0 : authMethods.password_login) || Boolean(recoveryToken);
|
|
126
|
+
const socialLoginEnabled = Boolean(authMethods === null || authMethods === void 0 ? void 0 : authMethods.social_login) && socialProviders.length > 0;
|
|
127
|
+
const passkeyLoginEnabled = Boolean(authMethods === null || authMethods === void 0 ? void 0 : authMethods.passkey_login);
|
|
128
|
+
const signupEnabled = Boolean(authMethods === null || authMethods === void 0 ? void 0 : authMethods.signup);
|
|
129
|
+
const passwordResetEnabled = Boolean(authMethods === null || authMethods === void 0 ? void 0 : authMethods.password_reset);
|
|
116
130
|
// --- Render ---
|
|
117
|
-
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: handleSubmitCredentials, onForgotPassword: () => navigate('/reset-request-password'), onSocialLogin: (provider) => startSocialLogin(provider), onPasskeyLogin: handlePasskeyLoginInitial, onSignUp: () => navigate('/signup'), disabled: submitting, initialIdentifier: recoveryEmail })), step === 'mfa' && mfaState && (_jsx(Box, { children: _jsx(MfaLoginComponent, { availableTypes: mfaState.availableTypes, identifier: mfaState.identifier, onSuccess: handleMfaSuccess, onCancel: handleMfaCancel }) }))] }));
|
|
131
|
+
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 }) }))] }));
|
|
118
132
|
}
|
|
@@ -101,9 +101,48 @@ export async function authenticateMfaWithPasskey() {
|
|
|
101
101
|
const credentialJson = serializeCredential(assertion);
|
|
102
102
|
return authenticateWithMFA({ credential: credentialJson });
|
|
103
103
|
}
|
|
104
|
-
|
|
104
|
+
function getCsrfTokenFromCookie() {
|
|
105
|
+
if (typeof document === 'undefined' || !document.cookie)
|
|
106
|
+
return null;
|
|
107
|
+
const match = document.cookie.match(/(?:^|; )csrftoken=([^;]+)/);
|
|
108
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
109
|
+
}
|
|
110
|
+
function submitSocialRedirectForm({ provider, callbackUrl, csrfToken }) {
|
|
111
|
+
const form = document.createElement('form');
|
|
112
|
+
form.method = 'POST';
|
|
113
|
+
form.action = `${HEADLESS_BASE}/auth/provider/redirect`;
|
|
114
|
+
form.style.display = 'none';
|
|
115
|
+
const fields = {
|
|
116
|
+
provider,
|
|
117
|
+
process: 'login',
|
|
118
|
+
callback_url: callbackUrl,
|
|
119
|
+
csrfmiddlewaretoken: csrfToken,
|
|
120
|
+
};
|
|
121
|
+
Object.entries(fields).forEach(([name, value]) => {
|
|
122
|
+
const input = document.createElement('input');
|
|
123
|
+
input.type = 'hidden';
|
|
124
|
+
input.name = name;
|
|
125
|
+
input.value = String(value);
|
|
126
|
+
form.appendChild(input);
|
|
127
|
+
});
|
|
128
|
+
document.body.appendChild(form);
|
|
129
|
+
form.submit();
|
|
130
|
+
}
|
|
131
|
+
export async function startSocialLogin(provider) {
|
|
105
132
|
if (typeof window === 'undefined') {
|
|
106
133
|
throw normaliseApiError(new Error('Auth.SOCIAL_LOGIN_NOT_IN_BROWSER'), 'Auth.SOCIAL_LOGIN_NOT_IN_BROWSER');
|
|
107
134
|
}
|
|
108
|
-
|
|
135
|
+
try {
|
|
136
|
+
// Ensures csrftoken cookie exists before form POST.
|
|
137
|
+
await apiClient.get('/api/csrf/');
|
|
138
|
+
}
|
|
139
|
+
catch (_a) {
|
|
140
|
+
// Continue; token might already be present.
|
|
141
|
+
}
|
|
142
|
+
const csrfToken = getCsrfTokenFromCookie();
|
|
143
|
+
if (!csrfToken) {
|
|
144
|
+
throw normaliseApiError(new Error('Auth.SOCIAL_LOGIN_FAILED'), 'Auth.SOCIAL_LOGIN_FAILED');
|
|
145
|
+
}
|
|
146
|
+
const callbackUrl = `${window.location.origin}/login`;
|
|
147
|
+
submitSocialRedirectForm({ provider, callbackUrl, csrfToken });
|
|
109
148
|
}
|
package/package.json
CHANGED
package/src/auth/AuthContext.jsx
CHANGED
|
@@ -6,16 +6,65 @@ import React, {
|
|
|
6
6
|
} from 'react';
|
|
7
7
|
import { ensureCsrfToken } from './apiClient'; // <--- IMPORT ADDED
|
|
8
8
|
import {
|
|
9
|
+
fetchAuthMethods,
|
|
9
10
|
fetchCurrentUser,
|
|
10
11
|
logoutSession,
|
|
11
12
|
} from './authApi';
|
|
12
13
|
|
|
13
14
|
export const AuthContext = createContext(null);
|
|
14
15
|
|
|
16
|
+
const DEFAULT_AUTH_METHODS = {
|
|
17
|
+
password_login: true,
|
|
18
|
+
password_reset: true,
|
|
19
|
+
signup: true,
|
|
20
|
+
password_change: true,
|
|
21
|
+
social_login: true,
|
|
22
|
+
social_providers: ['google', 'microsoft'],
|
|
23
|
+
passkey_login: true,
|
|
24
|
+
passkeys_manage: true,
|
|
25
|
+
mfa_totp: true,
|
|
26
|
+
mfa_recovery_codes: true,
|
|
27
|
+
mfa_enabled: true,
|
|
28
|
+
};
|
|
29
|
+
|
|
15
30
|
export const AuthProvider = ({ children }) => {
|
|
16
31
|
const [user, setUser] = useState(null);
|
|
32
|
+
const [authMethods, setAuthMethods] = useState(DEFAULT_AUTH_METHODS);
|
|
17
33
|
const [loading, setLoading] = useState(true);
|
|
18
34
|
|
|
35
|
+
const mapUserFromApi = (data) => {
|
|
36
|
+
const profile = data?.profile || {};
|
|
37
|
+
return {
|
|
38
|
+
...data,
|
|
39
|
+
id: data?.id,
|
|
40
|
+
username: data?.username,
|
|
41
|
+
email: data?.email,
|
|
42
|
+
first_name: data?.first_name,
|
|
43
|
+
last_name: data?.last_name,
|
|
44
|
+
role: data?.role ?? profile?.role ?? null,
|
|
45
|
+
language: data?.language ?? profile?.language ?? 'en',
|
|
46
|
+
is_superuser: Boolean(data?.is_superuser),
|
|
47
|
+
is_new: Boolean(data?.is_new ?? profile?.is_new),
|
|
48
|
+
is_invited: Boolean(data?.is_invited ?? profile?.is_invited),
|
|
49
|
+
accepted_privacy_statement: Boolean(
|
|
50
|
+
data?.accepted_privacy_statement ?? profile?.accepted_privacy_statement
|
|
51
|
+
),
|
|
52
|
+
accepted_convenience_cookies: Boolean(
|
|
53
|
+
data?.accepted_convenience_cookies ?? profile?.accepted_convenience_cookies
|
|
54
|
+
),
|
|
55
|
+
is_support_agent: Boolean(data?.is_support_agent ?? profile?.is_support_agent),
|
|
56
|
+
support_contact_id: data?.support_contact_id ?? profile?.support_contact_id ?? null,
|
|
57
|
+
security_state: data?.security_state,
|
|
58
|
+
available_roles: data?.available_roles || [],
|
|
59
|
+
ui_permissions: data?.ui_permissions || {},
|
|
60
|
+
can_manage_support_agents: Boolean(data?.can_manage_support_agents),
|
|
61
|
+
can_manage: Boolean(data?.can_manage),
|
|
62
|
+
is_active: data?.is_active,
|
|
63
|
+
last_login: data?.last_login,
|
|
64
|
+
date_joined: data?.date_joined,
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
19
68
|
useEffect(() => {
|
|
20
69
|
let isMounted = true;
|
|
21
70
|
|
|
@@ -24,23 +73,21 @@ export const AuthProvider = ({ children }) => {
|
|
|
24
73
|
// 1) Ensure CSRF cookie exists using the specific client
|
|
25
74
|
await ensureCsrfToken();
|
|
26
75
|
|
|
27
|
-
// 2) Load
|
|
76
|
+
// 2) Load auth methods (public)
|
|
77
|
+
try {
|
|
78
|
+
const methods = await fetchAuthMethods();
|
|
79
|
+
if (isMounted && methods && typeof methods === 'object') {
|
|
80
|
+
setAuthMethods((prev) => ({ ...prev, ...methods }));
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// Keep defaults; login UI remains usable.
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 3) Load user
|
|
28
87
|
const data = await fetchCurrentUser();
|
|
29
88
|
|
|
30
89
|
if (isMounted) {
|
|
31
|
-
|
|
32
|
-
setUser({
|
|
33
|
-
id: data.id,
|
|
34
|
-
username: data.username,
|
|
35
|
-
email: data.email,
|
|
36
|
-
first_name: data.first_name,
|
|
37
|
-
last_name: data.last_name,
|
|
38
|
-
role: data.role,
|
|
39
|
-
is_superuser: data.is_superuser,
|
|
40
|
-
security_state: data.security_state,
|
|
41
|
-
available_roles: data.available_roles,
|
|
42
|
-
ui_permissions: data.ui_permissions,
|
|
43
|
-
});
|
|
90
|
+
setUser(mapUserFromApi(data));
|
|
44
91
|
}
|
|
45
92
|
} catch (err) {
|
|
46
93
|
// Silent failure on 401/403 is expected (user not logged in)
|
|
@@ -58,7 +105,7 @@ export const AuthProvider = ({ children }) => {
|
|
|
58
105
|
const login = (userData) => {
|
|
59
106
|
setUser((prev) => ({
|
|
60
107
|
...prev,
|
|
61
|
-
...userData,
|
|
108
|
+
...mapUserFromApi(userData),
|
|
62
109
|
}));
|
|
63
110
|
};
|
|
64
111
|
|
|
@@ -77,6 +124,7 @@ export const AuthProvider = ({ children }) => {
|
|
|
77
124
|
<AuthContext.Provider
|
|
78
125
|
value={{
|
|
79
126
|
user,
|
|
127
|
+
authMethods,
|
|
80
128
|
loading,
|
|
81
129
|
login,
|
|
82
130
|
logout,
|
|
@@ -85,4 +133,4 @@ export const AuthProvider = ({ children }) => {
|
|
|
85
133
|
{children}
|
|
86
134
|
</AuthContext.Provider>
|
|
87
135
|
);
|
|
88
|
-
};
|
|
136
|
+
};
|
package/src/auth/authApi.jsx
CHANGED
|
@@ -18,6 +18,11 @@ export async function fetchCurrentUser() {
|
|
|
18
18
|
return res.data;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export async function fetchAuthMethods() {
|
|
22
|
+
const res = await apiClient.get('/api/auth-methods/');
|
|
23
|
+
return res.data || {};
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
export async function updateUserProfile(data) {
|
|
22
27
|
try {
|
|
23
28
|
const res = await apiClient.patch(`${USERS_BASE}/current/`, data);
|
|
@@ -27,6 +32,22 @@ export async function updateUserProfile(data) {
|
|
|
27
32
|
}
|
|
28
33
|
}
|
|
29
34
|
|
|
35
|
+
function normalizeStatementText(data) {
|
|
36
|
+
if (typeof data === 'string') return data;
|
|
37
|
+
if (data && typeof data.content === 'string') return data.content;
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function fetchPrivacyStatement() {
|
|
42
|
+
const res = await apiClient.get('/api/utils/privacy/');
|
|
43
|
+
return normalizeStatementText(res.data);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function fetchCookieStatement() {
|
|
47
|
+
const res = await apiClient.get('/api/utils/cookie/');
|
|
48
|
+
return normalizeStatementText(res.data);
|
|
49
|
+
}
|
|
50
|
+
|
|
30
51
|
export async function fetchHeadlessSession() {
|
|
31
52
|
const res = await apiClient.get(`${HEADLESS_BASE}/auth/session`);
|
|
32
53
|
return res.data;
|
|
@@ -411,4 +432,4 @@ export async function updateUserSupportStatus(userId, isSupportAgent) {
|
|
|
411
432
|
} catch (error) {
|
|
412
433
|
throw normaliseApiError(error, 'Auth.USER_SUPPORT_UPDATE_FAILED');
|
|
413
434
|
}
|
|
414
|
-
}
|
|
435
|
+
}
|