@micha.bigler/ui-core-micha 1.4.20 → 1.4.22
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 +0 -1
- package/dist/auth/authApi.js +137 -378
- package/dist/components/MFAComponent.js +7 -7
- package/dist/components/MfaLoginComponent.js +6 -5
- package/dist/components/PasskeysComponent.js +5 -5
- package/dist/components/SecurityComponent.js +4 -3
- package/dist/components/SupportRecoveryRequestsTab.js +4 -4
- package/dist/components/UserInviteComponent.js +38 -0
- package/dist/components/UserListComponent.js +83 -0
- package/dist/pages/AccountPage.js +67 -23
- package/dist/pages/LoginPage.js +6 -5
- package/dist/pages/PasswordInvitePage.js +3 -3
- package/dist/pages/SignUpPage.js +3 -3
- package/dist/utils/authService.js +53 -0
- package/dist/utils/errors.js +33 -0
- package/dist/utils/webauthn.js +44 -0
- package/package.json +1 -1
- package/src/auth/AuthContext.jsx +0 -1
- package/src/auth/authApi.jsx +143 -478
- package/src/components/MFAComponent.jsx +7 -7
- package/src/components/MfaLoginComponent.jsx +6 -5
- package/src/components/PasskeysComponent.jsx +5 -5
- package/src/components/SecurityComponent.jsx +4 -3
- package/src/components/SupportRecoveryRequestsTab.jsx +4 -4
- package/src/components/UserInviteComponent.jsx +69 -0
- package/src/components/UserListComponent.jsx +167 -0
- package/src/pages/AccountPage.jsx +140 -47
- package/src/pages/LoginPage.jsx +6 -5
- package/src/pages/PasswordInvitePage.jsx +3 -3
- package/src/pages/SignUpPage.jsx +3 -3
- package/src/utils/authService.js +68 -0
- package/src/utils/errors.js +39 -0
- package/src/utils/webauthn.js +51 -0
package/src/auth/authApi.jsx
CHANGED
|
@@ -1,134 +1,18 @@
|
|
|
1
1
|
import apiClient from './apiClient';
|
|
2
2
|
import { HEADLESS_BASE, USERS_BASE, ACCESS_CODES_BASE } from './authConfig';
|
|
3
|
+
import { normaliseApiError } from '../utils/errors'; // Beachte den Pfad zu deiner errors.js
|
|
3
4
|
|
|
4
|
-
//
|
|
5
|
-
// WebAuthn Serialization Helpers
|
|
6
|
-
// -----------------------------
|
|
7
|
-
function bufferToBase64URL(buffer) {
|
|
8
|
-
const bytes = new Uint8Array(buffer);
|
|
9
|
-
let str = '';
|
|
10
|
-
for (const char of bytes) {
|
|
11
|
-
str += String.fromCharCode(char);
|
|
12
|
-
}
|
|
13
|
-
const base64 = btoa(str);
|
|
14
|
-
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function serializeCredential(credential) {
|
|
18
|
-
// Wir nutzen IMMER die manuelle Serialisierung, um sicherzugehen,
|
|
19
|
-
// dass alles Base64URL-kodiert ist (kompatibel mit Allauth).
|
|
20
|
-
const p = {
|
|
21
|
-
id: credential.id,
|
|
22
|
-
rawId: bufferToBase64URL(credential.rawId),
|
|
23
|
-
type: credential.type,
|
|
24
|
-
response: {
|
|
25
|
-
clientDataJSON: bufferToBase64URL(credential.response.clientDataJSON),
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
if (credential.response.attestationObject) {
|
|
30
|
-
p.response.attestationObject = bufferToBase64URL(credential.response.attestationObject);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (credential.response.authenticatorData) {
|
|
34
|
-
p.response.authenticatorData = bufferToBase64URL(credential.response.authenticatorData);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (credential.response.signature) {
|
|
38
|
-
p.response.signature = bufferToBase64URL(credential.response.signature);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (credential.response.userHandle) {
|
|
42
|
-
p.response.userHandle = bufferToBase64URL(credential.response.userHandle);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Extension Results direkt übernehmen, falls vorhanden
|
|
46
|
-
if (typeof credential.getClientExtensionResults === 'function') {
|
|
47
|
-
p.clientExtensionResults = credential.getClientExtensionResults();
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return p;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function normaliseApiError(error, defaultCode = 'Auth.GENERIC_ERROR') {
|
|
54
|
-
const info = extractErrorInfo(error);
|
|
55
|
-
const code = info.code || defaultCode;
|
|
56
|
-
const message = info.message || code || defaultCode;
|
|
57
|
-
|
|
58
|
-
const err = new Error(message);
|
|
59
|
-
err.code = code;
|
|
60
|
-
err.status = info.status;
|
|
61
|
-
err.raw = info.raw;
|
|
62
|
-
|
|
63
|
-
return err;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function mapAllauthDetailToCode(detail) {
|
|
67
|
-
if (!detail || typeof detail !== 'string') return null;
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function extractErrorInfo(error) {
|
|
72
|
-
const status = error.response?.status ?? null;
|
|
73
|
-
const data = error.response?.data ?? null;
|
|
74
|
-
|
|
75
|
-
if (!data) {
|
|
76
|
-
return { status, code: null, message: error.message || null, raw: null };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (typeof data.code === 'string') {
|
|
80
|
-
return { status, code: data.code, message: null, raw: data };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (typeof data.detail === 'string') {
|
|
84
|
-
const mapped = mapAllauthDetailToCode(data.detail);
|
|
85
|
-
return { status, code: mapped, message: data.detail, raw: data };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (Array.isArray(data.non_field_errors) && data.non_field_errors.length > 0) {
|
|
89
|
-
const msg = data.non_field_errors[0];
|
|
90
|
-
const mapped = mapAllauthDetailToCode(msg);
|
|
91
|
-
return { status, code: mapped, message: msg, raw: data };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return { status, code: null, message: null, raw: data };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// -----------------------------
|
|
98
|
-
// CSRF helper
|
|
99
|
-
// -----------------------------
|
|
5
|
+
// --- Internal Helper for CSRF ---
|
|
100
6
|
function getCsrfToken() {
|
|
101
|
-
if (typeof document === 'undefined' || !document.cookie)
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
// Robust regex for CSRF token
|
|
7
|
+
if (typeof document === 'undefined' || !document.cookie) return null;
|
|
105
8
|
const match = document.cookie.match(/(?:^|; )csrftoken=([^;]+)/);
|
|
106
9
|
return match ? match[1] : null;
|
|
107
10
|
}
|
|
108
11
|
|
|
109
12
|
// -----------------------------
|
|
110
|
-
//
|
|
13
|
+
// Session & User Core
|
|
111
14
|
// -----------------------------
|
|
112
|
-
const hasJsonWebAuthn =
|
|
113
|
-
typeof window !== 'undefined' &&
|
|
114
|
-
typeof window.PublicKeyCredential !== 'undefined' &&
|
|
115
|
-
typeof window.PublicKeyCredential.parseCreationOptionsFromJSON === 'function' &&
|
|
116
|
-
typeof window.PublicKeyCredential.parseRequestOptionsFromJSON === 'function';
|
|
117
|
-
|
|
118
|
-
function ensureWebAuthnSupport() {
|
|
119
|
-
if (
|
|
120
|
-
typeof window === 'undefined' ||
|
|
121
|
-
typeof navigator === 'undefined' ||
|
|
122
|
-
!window.PublicKeyCredential ||
|
|
123
|
-
!navigator.credentials
|
|
124
|
-
) {
|
|
125
|
-
throw normaliseApiError(new Error('Auth.PASSKEY_NOT_SUPPORTED'), 'Auth.PASSKEY_NOT_SUPPORTED');
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
15
|
|
|
129
|
-
// -----------------------------
|
|
130
|
-
// User-related helpers
|
|
131
|
-
// -----------------------------
|
|
132
16
|
export async function fetchCurrentUser() {
|
|
133
17
|
const res = await apiClient.get(`${USERS_BASE}/current/`);
|
|
134
18
|
return res.data;
|
|
@@ -136,7 +20,6 @@ export async function fetchCurrentUser() {
|
|
|
136
20
|
|
|
137
21
|
export async function updateUserProfile(data) {
|
|
138
22
|
try {
|
|
139
|
-
// CHANGED: axios -> apiClient
|
|
140
23
|
const res = await apiClient.patch(`${USERS_BASE}/current/`, data);
|
|
141
24
|
return res.data;
|
|
142
25
|
} catch (error) {
|
|
@@ -144,128 +27,105 @@ export async function updateUserProfile(data) {
|
|
|
144
27
|
}
|
|
145
28
|
}
|
|
146
29
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
export async function requestPasswordReset(email) {
|
|
151
|
-
try {
|
|
152
|
-
await apiClient.post(
|
|
153
|
-
`${USERS_BASE}/reset-request/`,
|
|
154
|
-
{ email }
|
|
155
|
-
);
|
|
156
|
-
} catch (error) {
|
|
157
|
-
throw normaliseApiError(error, 'Auth.RESET_REQUEST_FAILED');
|
|
158
|
-
}
|
|
30
|
+
export async function fetchHeadlessSession() {
|
|
31
|
+
const res = await apiClient.get(`${HEADLESS_BASE}/auth/session`);
|
|
32
|
+
return res.data;
|
|
159
33
|
}
|
|
160
34
|
|
|
161
|
-
export async function
|
|
35
|
+
export async function logoutSession() {
|
|
162
36
|
try {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
);
|
|
167
|
-
} catch (error) {
|
|
168
|
-
throw normaliseApiError(error, 'Auth.RESET_WITH_KEY_FAILED');
|
|
169
|
-
}
|
|
170
|
-
}
|
|
37
|
+
const headers = {};
|
|
38
|
+
const token = getCsrfToken();
|
|
39
|
+
if (token) headers['X-CSRFToken'] = token;
|
|
171
40
|
|
|
172
|
-
|
|
173
|
-
try {
|
|
174
|
-
await apiClient.post(
|
|
175
|
-
`${HEADLESS_BASE}/account/password/change`,
|
|
176
|
-
{
|
|
177
|
-
current_password: currentPassword,
|
|
178
|
-
new_password: newPassword,
|
|
179
|
-
}
|
|
180
|
-
);
|
|
41
|
+
await apiClient.delete(`${HEADLESS_BASE}/auth/session`, { headers });
|
|
181
42
|
} catch (error) {
|
|
182
|
-
|
|
43
|
+
// 401/404 beim Logout ignorieren wir
|
|
44
|
+
if (error.response && [401, 404, 410].includes(error.response.status)) return;
|
|
45
|
+
console.error('Logout error:', error);
|
|
183
46
|
}
|
|
184
47
|
}
|
|
185
48
|
|
|
186
49
|
// -----------------------------
|
|
187
|
-
//
|
|
50
|
+
// Authentication: Password & MFA
|
|
188
51
|
// -----------------------------
|
|
189
|
-
export async function logoutSession() {
|
|
190
|
-
try {
|
|
191
|
-
const headers = {};
|
|
192
|
-
const csrfToken = getCsrfToken();
|
|
193
|
-
if (csrfToken) {
|
|
194
|
-
headers['X-CSRFToken'] = csrfToken;
|
|
195
|
-
}
|
|
196
52
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
);
|
|
53
|
+
export async function loginWithPassword(email, password) {
|
|
54
|
+
try {
|
|
55
|
+
await apiClient.post(`${HEADLESS_BASE}/auth/login`, { email, password });
|
|
56
|
+
// Nach erfolgreichem Login User holen
|
|
57
|
+
const user = await fetchCurrentUser();
|
|
58
|
+
return { user, needsMfa: false };
|
|
202
59
|
} catch (error) {
|
|
203
|
-
|
|
204
|
-
|
|
60
|
+
const status = error.response?.status;
|
|
61
|
+
const body = error.response?.data;
|
|
62
|
+
|
|
63
|
+
// Prüfen auf Allauth Headless MFA Flow (401 + flows)
|
|
64
|
+
const flows = body?.data?.flows || body?.flows || [];
|
|
65
|
+
const mfaFlow = Array.isArray(flows) ? flows.find((f) => f.id === 'mfa_authenticate') : null;
|
|
66
|
+
|
|
67
|
+
if (status === 401 && mfaFlow && mfaFlow.is_pending) {
|
|
68
|
+
return { needsMfa: true, availableTypes: mfaFlow.types || [] };
|
|
205
69
|
}
|
|
206
|
-
|
|
207
|
-
|
|
70
|
+
|
|
71
|
+
// 409 = Already logged in
|
|
72
|
+
if (status === 409) {
|
|
73
|
+
const user = await fetchCurrentUser();
|
|
74
|
+
return { user, needsMfa: false };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
throw normaliseApiError(error, 'Auth.LOGIN_FAILED');
|
|
208
78
|
}
|
|
209
79
|
}
|
|
210
80
|
|
|
211
|
-
export async function
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
81
|
+
export async function authenticateWithMFA({ code, credential }) {
|
|
82
|
+
const payload = {};
|
|
83
|
+
if (code) payload.code = code;
|
|
84
|
+
if (credential) payload.credential = credential;
|
|
215
85
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
throw normaliseApiError(
|
|
222
|
-
new Error('Auth.SOCIAL_LOGIN_NOT_IN_BROWSER'),
|
|
223
|
-
'Auth.SOCIAL_LOGIN_NOT_IN_BROWSER'
|
|
224
|
-
);
|
|
86
|
+
try {
|
|
87
|
+
const res = await apiClient.post(`${HEADLESS_BASE}/auth/2fa/authenticate`, payload);
|
|
88
|
+
return res.data;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
throw normaliseApiError(error, 'Auth.MFA_AUTHENTICATE_FAILED');
|
|
225
91
|
}
|
|
226
|
-
window.location.href = `/accounts/${provider}/login/?process=login`;
|
|
227
92
|
}
|
|
228
93
|
|
|
229
94
|
// -----------------------------
|
|
230
|
-
//
|
|
95
|
+
// Password Management
|
|
231
96
|
// -----------------------------
|
|
232
|
-
|
|
97
|
+
|
|
98
|
+
export async function requestPasswordReset(email) {
|
|
233
99
|
try {
|
|
234
|
-
|
|
235
|
-
`${ACCESS_CODES_BASE}/validate/`,
|
|
236
|
-
{ code }
|
|
237
|
-
);
|
|
238
|
-
return res.data;
|
|
100
|
+
await apiClient.post(`${USERS_BASE}/reset-request/`, { email });
|
|
239
101
|
} catch (error) {
|
|
240
|
-
throw normaliseApiError(error, 'Auth.
|
|
102
|
+
throw normaliseApiError(error, 'Auth.RESET_REQUEST_FAILED');
|
|
241
103
|
}
|
|
242
104
|
}
|
|
243
105
|
|
|
244
|
-
export async function
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
106
|
+
export async function resetPasswordWithKey(key, newPassword) {
|
|
107
|
+
try {
|
|
108
|
+
await apiClient.post(`${HEADLESS_BASE}/auth/password/reset/key`, { key, password: newPassword });
|
|
109
|
+
} catch (error) {
|
|
110
|
+
throw normaliseApiError(error, 'Auth.RESET_WITH_KEY_FAILED');
|
|
248
111
|
}
|
|
112
|
+
}
|
|
249
113
|
|
|
114
|
+
export async function changePassword(currentPassword, newPassword) {
|
|
250
115
|
try {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
);
|
|
255
|
-
return res.data;
|
|
116
|
+
await apiClient.post(`${HEADLESS_BASE}/account/password/change`, {
|
|
117
|
+
current_password: currentPassword,
|
|
118
|
+
new_password: newPassword,
|
|
119
|
+
});
|
|
256
120
|
} catch (error) {
|
|
257
|
-
throw normaliseApiError(error, 'Auth.
|
|
121
|
+
throw normaliseApiError(error, 'Auth.PASSWORD_CHANGE_FAILED');
|
|
258
122
|
}
|
|
259
123
|
}
|
|
260
124
|
|
|
261
|
-
//
|
|
262
|
-
// Custom password-reset via uid/token
|
|
263
|
-
// -----------------------------
|
|
125
|
+
// Custom UID/Token Reset Flow
|
|
264
126
|
export async function verifyResetToken(uid, token) {
|
|
265
127
|
try {
|
|
266
|
-
const res = await apiClient.get(
|
|
267
|
-
`${USERS_BASE}/password-reset/${uid}/${token}/`
|
|
268
|
-
);
|
|
128
|
+
const res = await apiClient.get(`${USERS_BASE}/password-reset/${uid}/${token}/`);
|
|
269
129
|
return res.data;
|
|
270
130
|
} catch (error) {
|
|
271
131
|
throw normaliseApiError(error, 'Auth.RESET_LINK_INVALID');
|
|
@@ -274,10 +134,7 @@ export async function verifyResetToken(uid, token) {
|
|
|
274
134
|
|
|
275
135
|
export async function setNewPassword(uid, token, newPassword) {
|
|
276
136
|
try {
|
|
277
|
-
const res = await apiClient.post(
|
|
278
|
-
`${USERS_BASE}/password-reset/${uid}/${token}/`,
|
|
279
|
-
{ new_password: newPassword }
|
|
280
|
-
);
|
|
137
|
+
const res = await apiClient.post(`${USERS_BASE}/password-reset/${uid}/${token}/`, { new_password: newPassword });
|
|
281
138
|
return res.data;
|
|
282
139
|
} catch (error) {
|
|
283
140
|
throw normaliseApiError(error, 'Auth.RESET_PASSWORD_FAILED');
|
|
@@ -285,118 +142,39 @@ export async function setNewPassword(uid, token, newPassword) {
|
|
|
285
142
|
}
|
|
286
143
|
|
|
287
144
|
// -----------------------------
|
|
288
|
-
// WebAuthn / Passkeys
|
|
145
|
+
// WebAuthn / Passkeys (API Calls Only)
|
|
289
146
|
// -----------------------------
|
|
290
|
-
async function registerPasskeyStart({ passwordless = true } = {}) {
|
|
291
|
-
ensureWebAuthnSupport();
|
|
292
|
-
const res = await apiClient.get(
|
|
293
|
-
`${HEADLESS_BASE}/account/authenticators/webauthn`,
|
|
294
|
-
{
|
|
295
|
-
params: passwordless ? { passwordless: true } : {},
|
|
296
|
-
}
|
|
297
|
-
);
|
|
298
|
-
const responseBody = res.data || {};
|
|
299
|
-
const payload = responseBody.data || responseBody;
|
|
300
|
-
const publicKeyJson =
|
|
301
|
-
(payload.creation_options && payload.creation_options.publicKey) ||
|
|
302
|
-
payload.publicKey ||
|
|
303
|
-
payload;
|
|
304
|
-
return publicKeyJson;
|
|
305
|
-
}
|
|
306
147
|
|
|
307
|
-
async function
|
|
308
|
-
const res = await apiClient.
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
148
|
+
export async function getPasskeyRegistrationOptions() {
|
|
149
|
+
const res = await apiClient.get(`${HEADLESS_BASE}/account/authenticators/webauthn`, {
|
|
150
|
+
params: { passwordless: true }
|
|
151
|
+
});
|
|
152
|
+
// Extrahiere nested data Struktur von Allauth
|
|
153
|
+
const data = res.data?.data || res.data;
|
|
154
|
+
return data.creation_options || data;
|
|
313
155
|
}
|
|
314
156
|
|
|
315
|
-
export async function
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
const publicKeyJson = await registerPasskeyStart({ passwordless: true });
|
|
324
|
-
let credential;
|
|
325
|
-
try {
|
|
326
|
-
const publicKeyOptions = window.PublicKeyCredential.parseCreationOptionsFromJSON(publicKeyJson);
|
|
327
|
-
credential = await navigator.credentials.create({ publicKey: publicKeyOptions });
|
|
328
|
-
} catch (err) {
|
|
329
|
-
if (err && err.name === 'NotAllowedError') {
|
|
330
|
-
throw normaliseApiError(err, 'Auth.PASSKEY_CREATION_CANCELLED');
|
|
331
|
-
}
|
|
332
|
-
throw err;
|
|
333
|
-
}
|
|
334
|
-
if (!credential) {
|
|
335
|
-
throw normaliseApiError(new Error('Auth.PASSKEY_CREATION_CANCELLED'), 'Auth.PASSKEY_CREATION_CANCELLED');
|
|
336
|
-
}
|
|
337
|
-
const credentialJson = serializeCredential(credential);
|
|
338
|
-
return registerPasskeyComplete(credentialJson, name);
|
|
157
|
+
export async function completePasskeyRegistration(credentialJson, name) {
|
|
158
|
+
const res = await apiClient.post(`${HEADLESS_BASE}/account/authenticators/webauthn`, {
|
|
159
|
+
credential: credentialJson,
|
|
160
|
+
name
|
|
161
|
+
});
|
|
162
|
+
return res.data;
|
|
339
163
|
}
|
|
340
164
|
|
|
341
|
-
async function
|
|
342
|
-
ensureWebAuthnSupport();
|
|
165
|
+
export async function getPasskeyLoginOptions() {
|
|
343
166
|
const res = await apiClient.get(`${HEADLESS_BASE}/auth/webauthn/login`);
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
const requestOptionsJson =
|
|
347
|
-
(payload.request_options && payload.request_options.publicKey) ||
|
|
348
|
-
payload.request_options ||
|
|
349
|
-
payload;
|
|
350
|
-
return requestOptionsJson;
|
|
167
|
+
const data = res.data?.data || res.data;
|
|
168
|
+
return data.request_options || data;
|
|
351
169
|
}
|
|
352
170
|
|
|
353
|
-
async function
|
|
354
|
-
const res = await apiClient.post(
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
);
|
|
171
|
+
export async function completePasskeyLogin(credentialJson) {
|
|
172
|
+
const res = await apiClient.post(`${HEADLESS_BASE}/auth/webauthn/login`, {
|
|
173
|
+
credential: credentialJson
|
|
174
|
+
});
|
|
358
175
|
return res.data;
|
|
359
176
|
}
|
|
360
177
|
|
|
361
|
-
export async function loginWithPasskey() {
|
|
362
|
-
ensureWebAuthnSupport();
|
|
363
|
-
if (!hasJsonWebAuthn) {
|
|
364
|
-
throw normaliseApiError(
|
|
365
|
-
new Error('Auth.PASSKEY_JSON_HELPERS_UNAVAILABLE'),
|
|
366
|
-
'Auth.PASSKEY_JSON_HELPERS_UNAVAILABLE'
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
const requestOptionsJson = await loginWithPasskeyStart();
|
|
370
|
-
let assertion;
|
|
371
|
-
try {
|
|
372
|
-
const publicKeyOptions = window.PublicKeyCredential.parseRequestOptionsFromJSON(requestOptionsJson);
|
|
373
|
-
assertion = await navigator.credentials.get({ publicKey: publicKeyOptions });
|
|
374
|
-
} catch (err) {
|
|
375
|
-
const e = new Error('Auth.PASSKEY_AUTH_CANCELLED');
|
|
376
|
-
e.code = 'Auth.PASSKEY_AUTH_CANCELLED';
|
|
377
|
-
throw e;
|
|
378
|
-
}
|
|
379
|
-
if (!assertion) {
|
|
380
|
-
const e = new Error('Auth.PASSKEY_AUTH_CANCELLED');
|
|
381
|
-
e.code = 'Auth.PASSKEY_AUTH_CANCELLED';
|
|
382
|
-
throw e;
|
|
383
|
-
}
|
|
384
|
-
const credentialJson = serializeCredential(assertion);
|
|
385
|
-
let postError = null;
|
|
386
|
-
try {
|
|
387
|
-
await loginWithPasskeyComplete(credentialJson);
|
|
388
|
-
} catch (err) {
|
|
389
|
-
postError = err;
|
|
390
|
-
}
|
|
391
|
-
try {
|
|
392
|
-
const user = await fetchCurrentUser();
|
|
393
|
-
return { user };
|
|
394
|
-
} catch (err) {
|
|
395
|
-
if (postError) throw normaliseApiError(postError, 'Auth.PASSKEY_FAILED');
|
|
396
|
-
throw normaliseApiError(err, 'Auth.PASSKEY_FAILED');
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
178
|
export async function fetchPasskeys() {
|
|
401
179
|
try {
|
|
402
180
|
const res = await apiClient.get(`${USERS_BASE}/passkeys/`);
|
|
@@ -408,7 +186,6 @@ export async function fetchPasskeys() {
|
|
|
408
186
|
|
|
409
187
|
export async function deletePasskey(id) {
|
|
410
188
|
try {
|
|
411
|
-
// CHANGED: axios -> apiClient
|
|
412
189
|
await apiClient.delete(`${USERS_BASE}/passkeys/${id}/`);
|
|
413
190
|
} catch (error) {
|
|
414
191
|
throw normaliseApiError(error, 'Auth.PASSKEY_DELETE_FAILED');
|
|
@@ -416,80 +193,24 @@ export async function deletePasskey(id) {
|
|
|
416
193
|
}
|
|
417
194
|
|
|
418
195
|
// -----------------------------
|
|
419
|
-
//
|
|
420
|
-
// -----------------------------
|
|
421
|
-
export async function authenticateWithMFA({ code, credential }) {
|
|
422
|
-
const payload = {};
|
|
423
|
-
if (code) payload.code = code;
|
|
424
|
-
if (credential) payload.credential = credential;
|
|
425
|
-
|
|
426
|
-
try {
|
|
427
|
-
const res = await apiClient.post(
|
|
428
|
-
`${HEADLESS_BASE}/auth/2fa/authenticate`,
|
|
429
|
-
payload
|
|
430
|
-
);
|
|
431
|
-
return res.data;
|
|
432
|
-
} catch (error) {
|
|
433
|
-
throw normaliseApiError(error, 'Auth.MFA_AUTHENTICATE_FAILED');
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// -----------------------------
|
|
438
|
-
// Authentication: password
|
|
196
|
+
// Authenticators (TOTP & Recovery)
|
|
439
197
|
// -----------------------------
|
|
440
|
-
export async function loginWithPassword(email, password) {
|
|
441
|
-
try {
|
|
442
|
-
await apiClient.post(
|
|
443
|
-
`${HEADLESS_BASE}/auth/login`,
|
|
444
|
-
{ email, password }
|
|
445
|
-
);
|
|
446
|
-
} catch (error) {
|
|
447
|
-
const status = error.response?.status;
|
|
448
|
-
const body = error.response?.data;
|
|
449
|
-
const flows = body?.data?.flows || body?.flows || [];
|
|
450
|
-
const mfaFlow = Array.isArray(flows)
|
|
451
|
-
? flows.find((f) => f.id === 'mfa_authenticate')
|
|
452
|
-
: null;
|
|
453
|
-
|
|
454
|
-
if (status === 401 && mfaFlow && mfaFlow.is_pending) {
|
|
455
|
-
return {
|
|
456
|
-
needsMfa: true,
|
|
457
|
-
availableTypes: mfaFlow.types || [],
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
if (status === 409) {
|
|
461
|
-
// Already logged in
|
|
462
|
-
} else {
|
|
463
|
-
throw normaliseApiError(error, 'Auth.LOGIN_FAILED');
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
const user = await fetchCurrentUser();
|
|
467
|
-
return { user, needsMfa: false };
|
|
468
|
-
}
|
|
469
198
|
|
|
470
|
-
// -----------------------------
|
|
471
|
-
// Authenticators & MFA
|
|
472
|
-
// -----------------------------
|
|
473
199
|
export async function fetchAuthenticators() {
|
|
474
200
|
const res = await apiClient.get(`${HEADLESS_BASE}/account/authenticators`);
|
|
475
201
|
const body = res.data || {};
|
|
476
|
-
|
|
477
|
-
return items;
|
|
202
|
+
return Array.isArray(body.data) ? body.data : (Array.isArray(body) ? body : []);
|
|
478
203
|
}
|
|
479
204
|
|
|
480
205
|
export async function requestTotpKey() {
|
|
481
206
|
try {
|
|
482
207
|
const res = await apiClient.get(`${HEADLESS_BASE}/account/authenticators/totp`);
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
return {
|
|
486
|
-
exists: true,
|
|
487
|
-
authenticator: data,
|
|
488
|
-
};
|
|
208
|
+
const data = res.data?.data || res.data;
|
|
209
|
+
return { exists: true, authenticator: data };
|
|
489
210
|
} catch (error) {
|
|
490
|
-
|
|
491
|
-
if (response?.status === 404) {
|
|
492
|
-
const meta = response.data?.meta || {};
|
|
211
|
+
// 404 bedeutet: Noch kein TOTP eingerichtet -> Wir bekommen das Secret zum Einrichten
|
|
212
|
+
if (error.response?.status === 404) {
|
|
213
|
+
const meta = error.response.data?.meta || {};
|
|
493
214
|
return {
|
|
494
215
|
exists: false,
|
|
495
216
|
secret: meta.secret,
|
|
@@ -502,11 +223,7 @@ export async function requestTotpKey() {
|
|
|
502
223
|
|
|
503
224
|
export async function activateTotp(code) {
|
|
504
225
|
try {
|
|
505
|
-
|
|
506
|
-
`${HEADLESS_BASE}/account/authenticators/totp`,
|
|
507
|
-
{ code }
|
|
508
|
-
);
|
|
509
|
-
return res.data;
|
|
226
|
+
await apiClient.post(`${HEADLESS_BASE}/account/authenticators/totp`, { code });
|
|
510
227
|
} catch (error) {
|
|
511
228
|
throw normaliseApiError(error, 'Auth.TOTP_ACTIVATE_FAILED');
|
|
512
229
|
}
|
|
@@ -514,9 +231,7 @@ export async function activateTotp(code) {
|
|
|
514
231
|
|
|
515
232
|
export async function deactivateTotp() {
|
|
516
233
|
try {
|
|
517
|
-
|
|
518
|
-
const res = await apiClient.delete(`${HEADLESS_BASE}/account/authenticators/totp`);
|
|
519
|
-
return res.data;
|
|
234
|
+
await apiClient.delete(`${HEADLESS_BASE}/account/authenticators/totp`);
|
|
520
235
|
} catch (error) {
|
|
521
236
|
throw normaliseApiError(error, 'Auth.TOTP_DEACTIVATE_FAILED');
|
|
522
237
|
}
|
|
@@ -524,20 +239,14 @@ export async function deactivateTotp() {
|
|
|
524
239
|
|
|
525
240
|
export async function fetchRecoveryCodes() {
|
|
526
241
|
try {
|
|
242
|
+
// Versuch, Codes zu laden
|
|
527
243
|
const res = await apiClient.get(`${HEADLESS_BASE}/account/authenticators/recovery-codes`);
|
|
528
|
-
|
|
529
|
-
const data = body.data || body;
|
|
530
|
-
return data;
|
|
244
|
+
return res.data?.data || res.data;
|
|
531
245
|
} catch (error) {
|
|
532
|
-
|
|
533
|
-
if (status === 404) {
|
|
534
|
-
const resPost = await apiClient.post(
|
|
535
|
-
|
|
536
|
-
{}
|
|
537
|
-
);
|
|
538
|
-
const body = resPost.data || {};
|
|
539
|
-
const data = body.data || body;
|
|
540
|
-
return data;
|
|
246
|
+
// 404 -> Noch keine Codes -> Generieren
|
|
247
|
+
if (error.response?.status === 404) {
|
|
248
|
+
const resPost = await apiClient.post(`${HEADLESS_BASE}/account/authenticators/recovery-codes`, {});
|
|
249
|
+
return resPost.data?.data || resPost.data;
|
|
541
250
|
}
|
|
542
251
|
throw normaliseApiError(error, 'Auth.RECOVERY_CODES_FETCH_FAILED');
|
|
543
252
|
}
|
|
@@ -545,122 +254,78 @@ export async function fetchRecoveryCodes() {
|
|
|
545
254
|
|
|
546
255
|
export async function generateRecoveryCodes() {
|
|
547
256
|
try {
|
|
548
|
-
const res = await apiClient.post(
|
|
549
|
-
|
|
550
|
-
{}
|
|
551
|
-
);
|
|
552
|
-
const body = res.data || {};
|
|
553
|
-
const data = body.data || body;
|
|
554
|
-
return data;
|
|
257
|
+
const res = await apiClient.post(`${HEADLESS_BASE}/account/authenticators/recovery-codes`, {});
|
|
258
|
+
return res.data?.data || res.data;
|
|
555
259
|
} catch (error) {
|
|
556
260
|
throw normaliseApiError(error, 'Auth.RECOVERY_CODES_GENERATE_FAILED');
|
|
557
261
|
}
|
|
558
262
|
}
|
|
559
263
|
|
|
560
|
-
|
|
264
|
+
// -----------------------------
|
|
265
|
+
// Invitations & Access Codes
|
|
266
|
+
// -----------------------------
|
|
267
|
+
|
|
268
|
+
export async function validateAccessCode(code) {
|
|
561
269
|
try {
|
|
562
|
-
const res = await apiClient.
|
|
270
|
+
const res = await apiClient.post(`${ACCESS_CODES_BASE}/validate/`, { code });
|
|
563
271
|
return res.data;
|
|
564
272
|
} catch (error) {
|
|
565
|
-
|
|
566
|
-
if (response?.status === 404) {
|
|
567
|
-
const resPost = await apiClient.post(
|
|
568
|
-
`${HEADLESS_BASE}/mfa/recovery_codes`,
|
|
569
|
-
{}
|
|
570
|
-
);
|
|
571
|
-
return resPost.data;
|
|
572
|
-
}
|
|
573
|
-
throw normaliseApiError(error, 'Auth.RECOVERY_CODES_FETCH_FAILED');
|
|
273
|
+
throw normaliseApiError(error, 'Auth.ACCESS_CODE_INVALID_OR_INACTIVE');
|
|
574
274
|
}
|
|
575
275
|
}
|
|
576
276
|
|
|
577
|
-
export function
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
277
|
+
export async function requestInviteWithCode(email, accessCode) {
|
|
278
|
+
const payload = { email };
|
|
279
|
+
if (accessCode) payload.access_code = accessCode;
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const res = await apiClient.post(`${USERS_BASE}/invite/`, payload);
|
|
283
|
+
return res.data;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
throw normaliseApiError(error, 'Auth.INVITE_FAILED');
|
|
286
|
+
}
|
|
582
287
|
}
|
|
583
288
|
|
|
289
|
+
// -----------------------------
|
|
290
|
+
// Recovery Support (Admin/Support Side)
|
|
291
|
+
// -----------------------------
|
|
292
|
+
|
|
584
293
|
export async function requestMfaSupportHelp(emailOrIdentifier, message = '') {
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
);
|
|
294
|
+
const res = await apiClient.post(`${USERS_BASE}/mfa/support-help/`, {
|
|
295
|
+
email: emailOrIdentifier,
|
|
296
|
+
message
|
|
297
|
+
});
|
|
590
298
|
return res.data;
|
|
591
299
|
}
|
|
592
300
|
|
|
593
301
|
export async function fetchRecoveryRequests(status = 'pending') {
|
|
594
|
-
const res = await apiClient.get(
|
|
595
|
-
'/api/support/recovery-requests/',
|
|
596
|
-
{ params: { status } }
|
|
597
|
-
);
|
|
302
|
+
const res = await apiClient.get('/api/support/recovery-requests/', { params: { status } });
|
|
598
303
|
return res.data;
|
|
599
304
|
}
|
|
600
305
|
|
|
601
306
|
export async function approveRecoveryRequest(id, supportNote) {
|
|
602
|
-
const res = await apiClient.post(
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
);
|
|
307
|
+
const res = await apiClient.post(`/api/support/recovery-requests/${id}/approve/`, {
|
|
308
|
+
support_note: supportNote || ''
|
|
309
|
+
});
|
|
606
310
|
return res.data;
|
|
607
311
|
}
|
|
608
312
|
|
|
609
313
|
export async function rejectRecoveryRequest(id, supportNote) {
|
|
610
|
-
const res = await apiClient.post(
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
);
|
|
314
|
+
const res = await apiClient.post(`/api/support/recovery-requests/${id}/reject/`, {
|
|
315
|
+
support_note: supportNote || ''
|
|
316
|
+
});
|
|
614
317
|
return res.data;
|
|
615
318
|
}
|
|
616
319
|
|
|
617
320
|
export async function loginWithRecoveryPassword(email, password, token) {
|
|
618
321
|
try {
|
|
619
|
-
await apiClient.post(
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
);
|
|
322
|
+
await apiClient.post(`/api/support/recovery-requests/recovery-login/${token}/`, {
|
|
323
|
+
email,
|
|
324
|
+
password
|
|
325
|
+
});
|
|
623
326
|
} catch (error) {
|
|
624
327
|
throw normaliseApiError(error, 'Auth.RECOVERY_LOGIN_FAILED');
|
|
625
328
|
}
|
|
626
329
|
const user = await fetchCurrentUser();
|
|
627
330
|
return { user, needsMfa: false };
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// -----------------------------
|
|
631
|
-
// Aggregated API object
|
|
632
|
-
// -----------------------------
|
|
633
|
-
export const authApi = {
|
|
634
|
-
fetchCurrentUser,
|
|
635
|
-
updateUserProfile,
|
|
636
|
-
loginWithPassword,
|
|
637
|
-
requestPasswordReset,
|
|
638
|
-
changePassword,
|
|
639
|
-
logoutSession,
|
|
640
|
-
startSocialLogin,
|
|
641
|
-
fetchHeadlessSession,
|
|
642
|
-
verifyResetToken,
|
|
643
|
-
setNewPassword,
|
|
644
|
-
loginWithPasskey,
|
|
645
|
-
registerPasskey,
|
|
646
|
-
fetchPasskeys,
|
|
647
|
-
deletePasskey,
|
|
648
|
-
authenticateWithMFA,
|
|
649
|
-
fetchAuthenticators,
|
|
650
|
-
requestTotpKey,
|
|
651
|
-
activateTotp,
|
|
652
|
-
deactivateTotp,
|
|
653
|
-
fetchRecoveryCodes,
|
|
654
|
-
generateRecoveryCodes,
|
|
655
|
-
fetchOrGenerateRecoveryCodes,
|
|
656
|
-
validateAccessCode,
|
|
657
|
-
requestInviteWithCode,
|
|
658
|
-
isStrongSession,
|
|
659
|
-
requestMfaSupportHelp,
|
|
660
|
-
fetchRecoveryRequests,
|
|
661
|
-
approveRecoveryRequest,
|
|
662
|
-
rejectRecoveryRequest,
|
|
663
|
-
loginWithRecoveryPassword,
|
|
664
|
-
};
|
|
665
|
-
|
|
666
|
-
export default authApi;
|
|
331
|
+
}
|