@micha.bigler/ui-core-micha 1.4.14 → 1.4.16
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 +15 -42
- package/dist/auth/apiClient.js +18 -0
- package/dist/auth/authApi.js +58 -165
- package/dist/auth/webauthnClient.js +3 -2
- package/dist/components/AccessCodeManager.js +4 -4
- package/dist/components/LoginForm.js +1 -1
- package/dist/components/MfaLoginComponent.js +1 -1
- package/dist/components/ProfileComponent.js +1 -1
- package/dist/components/SecurityComponent.js +2 -2
- package/dist/components/SupportRecoveryRequestsTab.js +2 -2
- package/dist/i18n/authTranslations.js +1 -137
- package/dist/pages/LoginPage.js +1 -1
- package/dist/pages/PasswordChangePage.js +2 -2
- package/dist/pages/PasswordResetRequestPage.js +1 -1
- package/dist/pages/SignUpPage.js +1 -1
- package/package.json +1 -1
- package/src/auth/AuthContext.jsx +16 -40
- package/src/auth/apiClient.jsx +20 -0
- package/src/auth/authApi.jsx +87 -295
- package/src/auth/webauthnClient.jsx +3 -2
- package/src/components/AccessCodeManager.jsx +4 -4
- package/src/components/LoginForm.jsx +1 -1
- package/src/components/MfaLoginComponent.jsx +0 -4
- package/src/components/ProfileComponent.jsx +1 -1
- package/src/components/SecurityComponent.jsx +2 -2
- package/src/components/SupportRecoveryRequestsTab.jsx +2 -2
- package/src/i18n/authTranslations.js +1 -153
- package/src/pages/LoginPage.jsx +1 -1
- package/src/pages/PasswordChangePage.jsx +3 -3
- package/src/pages/PasswordResetRequestPage.jsx +2 -2
- package/src/pages/SignUpPage.jsx +3 -3
package/src/auth/authApi.jsx
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import apiClient from './apiClient';
|
|
2
2
|
import { HEADLESS_BASE, USERS_BASE, ACCESS_CODES_BASE } from './authConfig';
|
|
3
3
|
|
|
4
4
|
// -----------------------------
|
|
5
5
|
// WebAuthn Serialization Helpers
|
|
6
6
|
// -----------------------------
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Converts an ArrayBuffer to a Base64URL encoded string.
|
|
10
|
-
* Required for manual credential serialization when .toJSON() is missing.
|
|
11
|
-
*/
|
|
12
7
|
function bufferToBase64URL(buffer) {
|
|
13
8
|
const bytes = new Uint8Array(buffer);
|
|
14
9
|
let str = '';
|
|
@@ -19,16 +14,11 @@ function bufferToBase64URL(buffer) {
|
|
|
19
14
|
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
20
15
|
}
|
|
21
16
|
|
|
22
|
-
/**
|
|
23
|
-
* Serializes a PublicKeyCredential into a JSON-compatible object.
|
|
24
|
-
* Acts as a polyfill for environments where credential.toJSON() is missing (e.g., Proton Pass).
|
|
25
|
-
*/
|
|
26
17
|
function serializeCredential(credential) {
|
|
27
18
|
if (typeof credential.toJSON === 'function') {
|
|
28
19
|
return credential.toJSON();
|
|
29
20
|
}
|
|
30
21
|
|
|
31
|
-
// Manual serialization for extensions that return incomplete objects
|
|
32
22
|
const p = {
|
|
33
23
|
id: credential.id,
|
|
34
24
|
rawId: bufferToBase64URL(credential.rawId),
|
|
@@ -39,26 +29,21 @@ function serializeCredential(credential) {
|
|
|
39
29
|
};
|
|
40
30
|
|
|
41
31
|
if (credential.response.attestationObject) {
|
|
42
|
-
// Registration specific
|
|
43
32
|
p.response.attestationObject = bufferToBase64URL(credential.response.attestationObject);
|
|
44
33
|
}
|
|
45
34
|
|
|
46
35
|
if (credential.response.authenticatorData) {
|
|
47
|
-
// Login specific
|
|
48
36
|
p.response.authenticatorData = bufferToBase64URL(credential.response.authenticatorData);
|
|
49
37
|
}
|
|
50
38
|
|
|
51
39
|
if (credential.response.signature) {
|
|
52
|
-
// Login specific
|
|
53
40
|
p.response.signature = bufferToBase64URL(credential.response.signature);
|
|
54
41
|
}
|
|
55
42
|
|
|
56
43
|
if (credential.response.userHandle) {
|
|
57
|
-
// Login specific
|
|
58
44
|
p.response.userHandle = bufferToBase64URL(credential.response.userHandle);
|
|
59
45
|
}
|
|
60
46
|
|
|
61
|
-
// Include clientExtensionResults if present
|
|
62
47
|
if (typeof credential.getClientExtensionResults === 'function') {
|
|
63
48
|
p.clientExtensionResults = credential.getClientExtensionResults();
|
|
64
49
|
}
|
|
@@ -68,7 +53,6 @@ function serializeCredential(credential) {
|
|
|
68
53
|
|
|
69
54
|
function normaliseApiError(error, defaultCode = 'Auth.GENERIC_ERROR') {
|
|
70
55
|
const info = extractErrorInfo(error);
|
|
71
|
-
|
|
72
56
|
const code = info.code || defaultCode;
|
|
73
57
|
const message = info.message || code || defaultCode;
|
|
74
58
|
|
|
@@ -80,13 +64,8 @@ function normaliseApiError(error, defaultCode = 'Auth.GENERIC_ERROR') {
|
|
|
80
64
|
return err;
|
|
81
65
|
}
|
|
82
66
|
|
|
83
|
-
|
|
84
67
|
function mapAllauthDetailToCode(detail) {
|
|
85
68
|
if (!detail || typeof detail !== 'string') return null;
|
|
86
|
-
// Optional: bekannte allauth-Texte auf eigene Codes mappen
|
|
87
|
-
// if (detail.includes('Unable to log in with provided credentials')) {
|
|
88
|
-
// return 'Auth.INVALID_CREDENTIALS';
|
|
89
|
-
// }
|
|
90
69
|
return null;
|
|
91
70
|
}
|
|
92
71
|
|
|
@@ -95,56 +74,27 @@ function extractErrorInfo(error) {
|
|
|
95
74
|
const data = error.response?.data ?? null;
|
|
96
75
|
|
|
97
76
|
if (!data) {
|
|
98
|
-
return {
|
|
99
|
-
status,
|
|
100
|
-
code: null,
|
|
101
|
-
message: error.message || null,
|
|
102
|
-
raw: null,
|
|
103
|
-
};
|
|
77
|
+
return { status, code: null, message: error.message || null, raw: null };
|
|
104
78
|
}
|
|
105
79
|
|
|
106
80
|
if (typeof data.code === 'string') {
|
|
107
|
-
return {
|
|
108
|
-
status,
|
|
109
|
-
code: data.code,
|
|
110
|
-
message: null,
|
|
111
|
-
raw: data,
|
|
112
|
-
};
|
|
81
|
+
return { status, code: data.code, message: null, raw: data };
|
|
113
82
|
}
|
|
114
83
|
|
|
115
84
|
if (typeof data.detail === 'string') {
|
|
116
85
|
const mapped = mapAllauthDetailToCode(data.detail);
|
|
117
|
-
return {
|
|
118
|
-
status,
|
|
119
|
-
code: mapped,
|
|
120
|
-
message: data.detail,
|
|
121
|
-
raw: data,
|
|
122
|
-
};
|
|
86
|
+
return { status, code: mapped, message: data.detail, raw: data };
|
|
123
87
|
}
|
|
124
88
|
|
|
125
|
-
if (
|
|
126
|
-
Array.isArray(data.non_field_errors) &&
|
|
127
|
-
data.non_field_errors.length > 0
|
|
128
|
-
) {
|
|
89
|
+
if (Array.isArray(data.non_field_errors) && data.non_field_errors.length > 0) {
|
|
129
90
|
const msg = data.non_field_errors[0];
|
|
130
91
|
const mapped = mapAllauthDetailToCode(msg);
|
|
131
|
-
return {
|
|
132
|
-
status,
|
|
133
|
-
code: mapped,
|
|
134
|
-
message: msg,
|
|
135
|
-
raw: data,
|
|
136
|
-
};
|
|
92
|
+
return { status, code: mapped, message: msg, raw: data };
|
|
137
93
|
}
|
|
138
94
|
|
|
139
|
-
return {
|
|
140
|
-
status,
|
|
141
|
-
code: null,
|
|
142
|
-
message: null,
|
|
143
|
-
raw: data,
|
|
144
|
-
};
|
|
95
|
+
return { status, code: null, message: null, raw: data };
|
|
145
96
|
}
|
|
146
97
|
|
|
147
|
-
|
|
148
98
|
// -----------------------------
|
|
149
99
|
// CSRF helper
|
|
150
100
|
// -----------------------------
|
|
@@ -152,7 +102,8 @@ function getCsrfToken() {
|
|
|
152
102
|
if (typeof document === 'undefined' || !document.cookie) {
|
|
153
103
|
return null;
|
|
154
104
|
}
|
|
155
|
-
|
|
105
|
+
// Robust regex for CSRF token
|
|
106
|
+
const match = document.cookie.match(/(?:^|; )csrftoken=([^;]+)/);
|
|
156
107
|
return match ? match[1] : null;
|
|
157
108
|
}
|
|
158
109
|
|
|
@@ -172,10 +123,7 @@ function ensureWebAuthnSupport() {
|
|
|
172
123
|
!window.PublicKeyCredential ||
|
|
173
124
|
!navigator.credentials
|
|
174
125
|
) {
|
|
175
|
-
throw normaliseApiError(
|
|
176
|
-
new Error('Auth.PASSKEY_NOT_SUPPORTED'),
|
|
177
|
-
'Auth.PASSKEY_NOT_SUPPORTED'
|
|
178
|
-
);
|
|
126
|
+
throw normaliseApiError(new Error('Auth.PASSKEY_NOT_SUPPORTED'), 'Auth.PASSKEY_NOT_SUPPORTED');
|
|
179
127
|
}
|
|
180
128
|
}
|
|
181
129
|
|
|
@@ -183,17 +131,14 @@ function ensureWebAuthnSupport() {
|
|
|
183
131
|
// User-related helpers
|
|
184
132
|
// -----------------------------
|
|
185
133
|
export async function fetchCurrentUser() {
|
|
186
|
-
const res = await
|
|
187
|
-
withCredentials: true,
|
|
188
|
-
});
|
|
134
|
+
const res = await apiClient.get(`${USERS_BASE}/current/`);
|
|
189
135
|
return res.data;
|
|
190
136
|
}
|
|
191
137
|
|
|
192
138
|
export async function updateUserProfile(data) {
|
|
193
139
|
try {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
});
|
|
140
|
+
// CHANGED: axios -> apiClient
|
|
141
|
+
const res = await apiClient.patch(`${USERS_BASE}/current/`, data);
|
|
197
142
|
return res.data;
|
|
198
143
|
} catch (error) {
|
|
199
144
|
throw normaliseApiError(error, 'Auth.PROFILE_UPDATE_FAILED');
|
|
@@ -203,45 +148,22 @@ export async function updateUserProfile(data) {
|
|
|
203
148
|
// -----------------------------
|
|
204
149
|
// Authentication: password
|
|
205
150
|
// -----------------------------
|
|
206
|
-
/*
|
|
207
|
-
export async function loginWithPassword(email, password) {
|
|
208
|
-
try {
|
|
209
|
-
await axios.post(
|
|
210
|
-
`${HEADLESS_BASE}/auth/login`,
|
|
211
|
-
{ email, password },
|
|
212
|
-
{ withCredentials: true },
|
|
213
|
-
);
|
|
214
|
-
} catch (error) {
|
|
215
|
-
if (error.response && error.response.status === 409) {
|
|
216
|
-
// Already logged in: continue and fetch current user
|
|
217
|
-
} else {
|
|
218
|
-
throw new Error(extractErrorMessage(error));
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const user = await fetchCurrentUser();
|
|
223
|
-
return { user };
|
|
224
|
-
}
|
|
225
|
-
*/
|
|
226
151
|
export async function requestPasswordReset(email) {
|
|
227
152
|
try {
|
|
228
|
-
await
|
|
153
|
+
await apiClient.post(
|
|
229
154
|
`${USERS_BASE}/reset-request/`,
|
|
230
|
-
{ email }
|
|
231
|
-
{ withCredentials: true },
|
|
155
|
+
{ email }
|
|
232
156
|
);
|
|
233
157
|
} catch (error) {
|
|
234
158
|
throw normaliseApiError(error, 'Auth.RESET_REQUEST_FAILED');
|
|
235
159
|
}
|
|
236
160
|
}
|
|
237
161
|
|
|
238
|
-
// (Falls du es noch brauchst: klassischer allauth-Key-Flow)
|
|
239
162
|
export async function resetPasswordWithKey(key, newPassword) {
|
|
240
163
|
try {
|
|
241
|
-
await
|
|
164
|
+
await apiClient.post(
|
|
242
165
|
`${HEADLESS_BASE}/auth/password/reset/key`,
|
|
243
|
-
{ key, password: newPassword }
|
|
244
|
-
{ withCredentials: true },
|
|
166
|
+
{ key, password: newPassword }
|
|
245
167
|
);
|
|
246
168
|
} catch (error) {
|
|
247
169
|
throw normaliseApiError(error, 'Auth.RESET_WITH_KEY_FAILED');
|
|
@@ -250,13 +172,12 @@ export async function resetPasswordWithKey(key, newPassword) {
|
|
|
250
172
|
|
|
251
173
|
export async function changePassword(currentPassword, newPassword) {
|
|
252
174
|
try {
|
|
253
|
-
await
|
|
175
|
+
await apiClient.post(
|
|
254
176
|
`${HEADLESS_BASE}/account/password/change`,
|
|
255
177
|
{
|
|
256
178
|
current_password: currentPassword,
|
|
257
179
|
new_password: newPassword,
|
|
258
|
-
}
|
|
259
|
-
{ withCredentials: true },
|
|
180
|
+
}
|
|
260
181
|
);
|
|
261
182
|
} catch (error) {
|
|
262
183
|
throw normaliseApiError(error, 'Auth.PASSWORD_CHANGE_FAILED');
|
|
@@ -274,16 +195,13 @@ export async function logoutSession() {
|
|
|
274
195
|
headers['X-CSRFToken'] = csrfToken;
|
|
275
196
|
}
|
|
276
197
|
|
|
277
|
-
|
|
198
|
+
// CHANGED: axios -> apiClient
|
|
199
|
+
await apiClient.delete(
|
|
278
200
|
`${HEADLESS_BASE}/auth/session`,
|
|
279
|
-
{
|
|
280
|
-
withCredentials: true,
|
|
281
|
-
headers,
|
|
282
|
-
},
|
|
201
|
+
{ headers }
|
|
283
202
|
);
|
|
284
203
|
} catch (error) {
|
|
285
204
|
if (error.response && [401, 404, 410].includes(error.response.status)) {
|
|
286
|
-
// Session already gone, nothing to do
|
|
287
205
|
return;
|
|
288
206
|
}
|
|
289
207
|
// eslint-disable-next-line no-console
|
|
@@ -292,9 +210,7 @@ export async function logoutSession() {
|
|
|
292
210
|
}
|
|
293
211
|
|
|
294
212
|
export async function fetchHeadlessSession() {
|
|
295
|
-
const res = await
|
|
296
|
-
withCredentials: true,
|
|
297
|
-
});
|
|
213
|
+
const res = await apiClient.get(`${HEADLESS_BASE}/auth/session`);
|
|
298
214
|
return res.data;
|
|
299
215
|
}
|
|
300
216
|
|
|
@@ -308,8 +224,6 @@ export function startSocialLogin(provider) {
|
|
|
308
224
|
'Auth.SOCIAL_LOGIN_NOT_IN_BROWSER'
|
|
309
225
|
);
|
|
310
226
|
}
|
|
311
|
-
|
|
312
|
-
// Classic allauth HTML flow
|
|
313
227
|
window.location.href = `/accounts/${provider}/login/?process=login`;
|
|
314
228
|
}
|
|
315
229
|
|
|
@@ -318,17 +232,16 @@ export function startSocialLogin(provider) {
|
|
|
318
232
|
// -----------------------------
|
|
319
233
|
export async function validateAccessCode(code) {
|
|
320
234
|
try {
|
|
321
|
-
const res = await
|
|
235
|
+
const res = await apiClient.post(
|
|
322
236
|
`${ACCESS_CODES_BASE}/validate/`,
|
|
323
|
-
{ code }
|
|
324
|
-
{ withCredentials: true },
|
|
237
|
+
{ code }
|
|
325
238
|
);
|
|
326
239
|
return res.data;
|
|
327
240
|
} catch (error) {
|
|
328
|
-
// Default für "irgendwas ist mit dem Code schief"
|
|
329
241
|
throw normaliseApiError(error, 'Auth.ACCESS_CODE_INVALID_OR_INACTIVE');
|
|
330
242
|
}
|
|
331
243
|
}
|
|
244
|
+
|
|
332
245
|
export async function requestInviteWithCode(email, accessCode) {
|
|
333
246
|
const payload = { email };
|
|
334
247
|
if (accessCode) {
|
|
@@ -336,14 +249,12 @@ export async function requestInviteWithCode(email, accessCode) {
|
|
|
336
249
|
}
|
|
337
250
|
|
|
338
251
|
try {
|
|
339
|
-
const res = await
|
|
252
|
+
const res = await apiClient.post(
|
|
340
253
|
`${USERS_BASE}/invite/`,
|
|
341
|
-
payload
|
|
342
|
-
{ withCredentials: true },
|
|
254
|
+
payload
|
|
343
255
|
);
|
|
344
256
|
return res.data;
|
|
345
257
|
} catch (error) {
|
|
346
|
-
// z.B. bei Netzwerkfehlern oder wenn Backend ausnahmsweise keinen code liefert
|
|
347
258
|
throw normaliseApiError(error, 'Auth.INVITE_FAILED');
|
|
348
259
|
}
|
|
349
260
|
}
|
|
@@ -353,23 +264,20 @@ export async function requestInviteWithCode(email, accessCode) {
|
|
|
353
264
|
// -----------------------------
|
|
354
265
|
export async function verifyResetToken(uid, token) {
|
|
355
266
|
try {
|
|
356
|
-
const res = await
|
|
357
|
-
`${USERS_BASE}/password-reset/${uid}/${token}
|
|
358
|
-
{ withCredentials: true },
|
|
267
|
+
const res = await apiClient.get(
|
|
268
|
+
`${USERS_BASE}/password-reset/${uid}/${token}/`
|
|
359
269
|
);
|
|
360
270
|
return res.data;
|
|
361
271
|
} catch (error) {
|
|
362
|
-
// Wenn der Link nicht passt oder Netzwerkfehler: generisch als "Link invalid"
|
|
363
272
|
throw normaliseApiError(error, 'Auth.RESET_LINK_INVALID');
|
|
364
273
|
}
|
|
365
274
|
}
|
|
366
275
|
|
|
367
276
|
export async function setNewPassword(uid, token, newPassword) {
|
|
368
277
|
try {
|
|
369
|
-
const res = await
|
|
278
|
+
const res = await apiClient.post(
|
|
370
279
|
`${USERS_BASE}/password-reset/${uid}/${token}/`,
|
|
371
|
-
{ new_password: newPassword }
|
|
372
|
-
{ withCredentials: true },
|
|
280
|
+
{ new_password: newPassword }
|
|
373
281
|
);
|
|
374
282
|
return res.data;
|
|
375
283
|
} catch (error) {
|
|
@@ -378,180 +286,121 @@ export async function setNewPassword(uid, token, newPassword) {
|
|
|
378
286
|
}
|
|
379
287
|
|
|
380
288
|
// -----------------------------
|
|
381
|
-
// WebAuthn / Passkeys
|
|
289
|
+
// WebAuthn / Passkeys
|
|
382
290
|
// -----------------------------
|
|
383
291
|
async function registerPasskeyStart({ passwordless = true } = {}) {
|
|
384
292
|
ensureWebAuthnSupport();
|
|
385
|
-
|
|
386
|
-
const res = await axios.get(
|
|
293
|
+
const res = await apiClient.get(
|
|
387
294
|
`${HEADLESS_BASE}/account/authenticators/webauthn`,
|
|
388
295
|
{
|
|
389
296
|
params: passwordless ? { passwordless: true } : {},
|
|
390
|
-
|
|
391
|
-
},
|
|
297
|
+
}
|
|
392
298
|
);
|
|
393
|
-
|
|
394
299
|
const responseBody = res.data || {};
|
|
395
|
-
|
|
396
|
-
// Handle nested 'data' wrapper if present, otherwise use body directly
|
|
397
300
|
const payload = responseBody.data || responseBody;
|
|
398
|
-
|
|
399
|
-
// Extract the inner publicKey structure required by the browser API
|
|
400
301
|
const publicKeyJson =
|
|
401
302
|
(payload.creation_options && payload.creation_options.publicKey) ||
|
|
402
303
|
payload.publicKey ||
|
|
403
304
|
payload;
|
|
404
|
-
|
|
405
305
|
return publicKeyJson;
|
|
406
306
|
}
|
|
407
307
|
|
|
408
|
-
|
|
409
308
|
async function registerPasskeyComplete(credentialJson, name = 'Passkey') {
|
|
410
|
-
const res = await
|
|
309
|
+
const res = await apiClient.post(
|
|
411
310
|
`${HEADLESS_BASE}/account/authenticators/webauthn`,
|
|
412
|
-
{
|
|
413
|
-
credential: credentialJson,
|
|
414
|
-
name,
|
|
415
|
-
},
|
|
416
|
-
{ withCredentials: true },
|
|
311
|
+
{ credential: credentialJson, name }
|
|
417
312
|
);
|
|
418
313
|
return res.data;
|
|
419
314
|
}
|
|
420
315
|
|
|
421
316
|
export async function registerPasskey(name = 'Passkey') {
|
|
422
317
|
ensureWebAuthnSupport();
|
|
423
|
-
|
|
424
318
|
if (!hasJsonWebAuthn) {
|
|
425
319
|
throw normaliseApiError(
|
|
426
320
|
new Error('Auth.PASSKEY_JSON_HELPERS_UNAVAILABLE'),
|
|
427
|
-
'Auth.PASSKEY_JSON_HELPERS_UNAVAILABLE'
|
|
321
|
+
'Auth.PASSKEY_JSON_HELPERS_UNAVAILABLE'
|
|
428
322
|
);
|
|
429
323
|
}
|
|
430
|
-
|
|
431
|
-
// ... (previous logic for registerPasskeyStart) ...
|
|
432
324
|
const publicKeyJson = await registerPasskeyStart({ passwordless: true });
|
|
433
|
-
|
|
434
325
|
let credential;
|
|
435
326
|
try {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
credential = await navigator.credentials.create({
|
|
440
|
-
publicKey: publicKeyOptions,
|
|
441
|
-
});
|
|
327
|
+
const publicKeyOptions = window.PublicKeyCredential.parseCreationOptionsFromJSON(publicKeyJson);
|
|
328
|
+
credential = await navigator.credentials.create({ publicKey: publicKeyOptions });
|
|
442
329
|
} catch (err) {
|
|
443
|
-
// ... (error handling) ...
|
|
444
330
|
if (err && err.name === 'NotAllowedError') {
|
|
445
331
|
throw normaliseApiError(err, 'Auth.PASSKEY_CREATION_CANCELLED');
|
|
446
332
|
}
|
|
447
333
|
throw err;
|
|
448
334
|
}
|
|
449
|
-
|
|
450
335
|
if (!credential) {
|
|
451
|
-
throw normaliseApiError(
|
|
452
|
-
new Error('Auth.PASSKEY_CREATION_CANCELLED'),
|
|
453
|
-
'Auth.PASSKEY_CREATION_CANCELLED',
|
|
454
|
-
);
|
|
336
|
+
throw normaliseApiError(new Error('Auth.PASSKEY_CREATION_CANCELLED'), 'Auth.PASSKEY_CREATION_CANCELLED');
|
|
455
337
|
}
|
|
456
|
-
|
|
457
|
-
// CHANGED: Use helper instead of direct .toJSON()
|
|
458
338
|
const credentialJson = serializeCredential(credential);
|
|
459
|
-
|
|
460
339
|
return registerPasskeyComplete(credentialJson, name);
|
|
461
340
|
}
|
|
462
341
|
|
|
463
|
-
// -----------------------------
|
|
464
|
-
// WebAuthn / Passkeys: login
|
|
465
|
-
// -----------------------------
|
|
466
342
|
async function loginWithPasskeyStart() {
|
|
467
343
|
ensureWebAuthnSupport();
|
|
468
|
-
|
|
469
|
-
const res = await axios.get(
|
|
470
|
-
`${HEADLESS_BASE}/auth/webauthn/login`,
|
|
471
|
-
{ withCredentials: true },
|
|
472
|
-
);
|
|
473
|
-
|
|
344
|
+
const res = await apiClient.get(`${HEADLESS_BASE}/auth/webauthn/login`);
|
|
474
345
|
const responseBody = res.data || {};
|
|
475
|
-
|
|
476
|
-
// Handle nested 'data' wrapper if present
|
|
477
346
|
const payload = responseBody.data || responseBody;
|
|
478
|
-
|
|
479
|
-
// Extract request options for authentication
|
|
480
347
|
const requestOptionsJson =
|
|
481
348
|
(payload.request_options && payload.request_options.publicKey) ||
|
|
482
349
|
payload.request_options ||
|
|
483
350
|
payload;
|
|
484
|
-
|
|
485
351
|
return requestOptionsJson;
|
|
486
352
|
}
|
|
487
353
|
|
|
488
354
|
async function loginWithPasskeyComplete(credentialJson) {
|
|
489
|
-
const res = await
|
|
355
|
+
const res = await apiClient.post(
|
|
490
356
|
`${HEADLESS_BASE}/auth/webauthn/login`,
|
|
491
|
-
{ credential: credentialJson }
|
|
492
|
-
{ withCredentials: true },
|
|
357
|
+
{ credential: credentialJson }
|
|
493
358
|
);
|
|
494
359
|
return res.data;
|
|
495
360
|
}
|
|
496
361
|
|
|
497
362
|
export async function loginWithPasskey() {
|
|
498
363
|
ensureWebAuthnSupport();
|
|
499
|
-
|
|
500
364
|
if (!hasJsonWebAuthn) {
|
|
501
365
|
throw normaliseApiError(
|
|
502
366
|
new Error('Auth.PASSKEY_JSON_HELPERS_UNAVAILABLE'),
|
|
503
|
-
'Auth.PASSKEY_JSON_HELPERS_UNAVAILABLE'
|
|
367
|
+
'Auth.PASSKEY_JSON_HELPERS_UNAVAILABLE'
|
|
504
368
|
);
|
|
505
369
|
}
|
|
506
|
-
|
|
507
370
|
const requestOptionsJson = await loginWithPasskeyStart();
|
|
508
|
-
|
|
509
371
|
let assertion;
|
|
510
372
|
try {
|
|
511
|
-
const publicKeyOptions =
|
|
512
|
-
window.PublicKeyCredential.parseRequestOptionsFromJSON(requestOptionsJson);
|
|
513
|
-
|
|
373
|
+
const publicKeyOptions = window.PublicKeyCredential.parseRequestOptionsFromJSON(requestOptionsJson);
|
|
514
374
|
assertion = await navigator.credentials.get({ publicKey: publicKeyOptions });
|
|
515
375
|
} catch (err) {
|
|
516
|
-
// Browser-interner Abbruch (z.B. Dialog weggeklickt)
|
|
517
376
|
const e = new Error('Auth.PASSKEY_AUTH_CANCELLED');
|
|
518
377
|
e.code = 'Auth.PASSKEY_AUTH_CANCELLED';
|
|
519
378
|
throw e;
|
|
520
379
|
}
|
|
521
|
-
|
|
522
380
|
if (!assertion) {
|
|
523
381
|
const e = new Error('Auth.PASSKEY_AUTH_CANCELLED');
|
|
524
382
|
e.code = 'Auth.PASSKEY_AUTH_CANCELLED';
|
|
525
383
|
throw e;
|
|
526
384
|
}
|
|
527
|
-
|
|
528
385
|
const credentialJson = serializeCredential(assertion);
|
|
529
|
-
|
|
530
386
|
let postError = null;
|
|
531
387
|
try {
|
|
532
388
|
await loginWithPasskeyComplete(credentialJson);
|
|
533
389
|
} catch (err) {
|
|
534
390
|
postError = err;
|
|
535
391
|
}
|
|
536
|
-
|
|
537
392
|
try {
|
|
538
393
|
const user = await fetchCurrentUser();
|
|
539
394
|
return { user };
|
|
540
395
|
} catch (err) {
|
|
541
|
-
if (postError)
|
|
542
|
-
throw normaliseApiError(postError, 'Auth.PASSKEY_FAILED');
|
|
543
|
-
}
|
|
396
|
+
if (postError) throw normaliseApiError(postError, 'Auth.PASSKEY_FAILED');
|
|
544
397
|
throw normaliseApiError(err, 'Auth.PASSKEY_FAILED');
|
|
545
398
|
}
|
|
546
399
|
}
|
|
547
400
|
|
|
548
|
-
|
|
549
401
|
export async function fetchPasskeys() {
|
|
550
402
|
try {
|
|
551
|
-
const res = await
|
|
552
|
-
`${USERS_BASE}/passkeys/`,
|
|
553
|
-
{ withCredentials: true },
|
|
554
|
-
);
|
|
403
|
+
const res = await apiClient.get(`${USERS_BASE}/passkeys/`);
|
|
555
404
|
return Array.isArray(res.data) ? res.data : [];
|
|
556
405
|
} catch (error) {
|
|
557
406
|
throw normaliseApiError(error, 'Auth.PASSKEY_LIST_FAILED');
|
|
@@ -560,10 +409,8 @@ export async function fetchPasskeys() {
|
|
|
560
409
|
|
|
561
410
|
export async function deletePasskey(id) {
|
|
562
411
|
try {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
{ withCredentials: true },
|
|
566
|
-
);
|
|
412
|
+
// CHANGED: axios -> apiClient
|
|
413
|
+
await apiClient.delete(`${USERS_BASE}/passkeys/${id}/`);
|
|
567
414
|
} catch (error) {
|
|
568
415
|
throw normaliseApiError(error, 'Auth.PASSKEY_DELETE_FAILED');
|
|
569
416
|
}
|
|
@@ -578,10 +425,9 @@ export async function authenticateWithMFA({ code, credential }) {
|
|
|
578
425
|
if (credential) payload.credential = credential;
|
|
579
426
|
|
|
580
427
|
try {
|
|
581
|
-
const res = await
|
|
428
|
+
const res = await apiClient.post(
|
|
582
429
|
`${HEADLESS_BASE}/auth/2fa/authenticate`,
|
|
583
|
-
payload
|
|
584
|
-
{ withCredentials: true },
|
|
430
|
+
payload
|
|
585
431
|
);
|
|
586
432
|
return res.data;
|
|
587
433
|
} catch (error) {
|
|
@@ -590,20 +436,17 @@ export async function authenticateWithMFA({ code, credential }) {
|
|
|
590
436
|
}
|
|
591
437
|
|
|
592
438
|
// -----------------------------
|
|
593
|
-
// Authentication: password
|
|
439
|
+
// Authentication: password
|
|
594
440
|
// -----------------------------
|
|
595
441
|
export async function loginWithPassword(email, password) {
|
|
596
442
|
try {
|
|
597
|
-
await
|
|
443
|
+
await apiClient.post(
|
|
598
444
|
`${HEADLESS_BASE}/auth/login`,
|
|
599
|
-
{ email, password }
|
|
600
|
-
{ withCredentials: true },
|
|
445
|
+
{ email, password }
|
|
601
446
|
);
|
|
602
447
|
} catch (error) {
|
|
603
448
|
const status = error.response?.status;
|
|
604
449
|
const body = error.response?.data;
|
|
605
|
-
|
|
606
|
-
// Flows können in body.data.flows oder body.flows liegen
|
|
607
450
|
const flows = body?.data?.flows || body?.flows || [];
|
|
608
451
|
const mfaFlow = Array.isArray(flows)
|
|
609
452
|
? flows.find((f) => f.id === 'mfa_authenticate')
|
|
@@ -612,55 +455,41 @@ export async function loginWithPassword(email, password) {
|
|
|
612
455
|
if (status === 401 && mfaFlow && mfaFlow.is_pending) {
|
|
613
456
|
return {
|
|
614
457
|
needsMfa: true,
|
|
615
|
-
availableTypes: mfaFlow.types || [],
|
|
458
|
+
availableTypes: mfaFlow.types || [],
|
|
616
459
|
};
|
|
617
460
|
}
|
|
618
|
-
|
|
619
461
|
if (status === 409) {
|
|
620
|
-
// Already logged in
|
|
462
|
+
// Already logged in
|
|
621
463
|
} else {
|
|
622
464
|
throw normaliseApiError(error, 'Auth.LOGIN_FAILED');
|
|
623
465
|
}
|
|
624
466
|
}
|
|
625
|
-
|
|
626
|
-
// Erfolg ohne MFA
|
|
627
467
|
const user = await fetchCurrentUser();
|
|
628
468
|
return { user, needsMfa: false };
|
|
629
469
|
}
|
|
630
470
|
|
|
631
|
-
//
|
|
471
|
+
// -----------------------------
|
|
472
|
+
// Authenticators & MFA
|
|
473
|
+
// -----------------------------
|
|
632
474
|
export async function fetchAuthenticators() {
|
|
633
|
-
const res = await
|
|
634
|
-
`${HEADLESS_BASE}/account/authenticators`,
|
|
635
|
-
{ withCredentials: true },
|
|
636
|
-
);
|
|
637
|
-
|
|
475
|
+
const res = await apiClient.get(`${HEADLESS_BASE}/account/authenticators`);
|
|
638
476
|
const body = res.data || {};
|
|
639
|
-
// Headless gibt i.d.R. { status, data: [...] } zurück
|
|
640
477
|
const items = Array.isArray(body.data) ? body.data : (Array.isArray(body) ? body : []);
|
|
641
478
|
return items;
|
|
642
479
|
}
|
|
643
480
|
|
|
644
|
-
// 2. TOTP Einrichtung starten (liefert Secret & QR-URL)
|
|
645
481
|
export async function requestTotpKey() {
|
|
646
482
|
try {
|
|
647
|
-
const res = await
|
|
648
|
-
`${HEADLESS_BASE}/account/authenticators/totp`,
|
|
649
|
-
{ withCredentials: true },
|
|
650
|
-
);
|
|
651
|
-
|
|
652
|
-
// Fall: TOTP existiert bereits -> 200
|
|
483
|
+
const res = await apiClient.get(`${HEADLESS_BASE}/account/authenticators/totp`);
|
|
653
484
|
const body = res.data || {};
|
|
654
485
|
const data = body.data || body;
|
|
655
|
-
|
|
656
486
|
return {
|
|
657
487
|
exists: true,
|
|
658
|
-
authenticator: data,
|
|
488
|
+
authenticator: data,
|
|
659
489
|
};
|
|
660
490
|
} catch (error) {
|
|
661
491
|
const { response } = error;
|
|
662
492
|
if (response?.status === 404) {
|
|
663
|
-
// Spezialfall von allauth: noch kein TOTP eingerichtet
|
|
664
493
|
const meta = response.data?.meta || {};
|
|
665
494
|
return {
|
|
666
495
|
exists: false,
|
|
@@ -674,10 +503,9 @@ export async function requestTotpKey() {
|
|
|
674
503
|
|
|
675
504
|
export async function activateTotp(code) {
|
|
676
505
|
try {
|
|
677
|
-
const res = await
|
|
506
|
+
const res = await apiClient.post(
|
|
678
507
|
`${HEADLESS_BASE}/account/authenticators/totp`,
|
|
679
|
-
{ code }
|
|
680
|
-
{ withCredentials: true },
|
|
508
|
+
{ code }
|
|
681
509
|
);
|
|
682
510
|
return res.data;
|
|
683
511
|
} catch (error) {
|
|
@@ -687,37 +515,26 @@ export async function activateTotp(code) {
|
|
|
687
515
|
|
|
688
516
|
export async function deactivateTotp() {
|
|
689
517
|
try {
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
{ withCredentials: true },
|
|
693
|
-
);
|
|
518
|
+
// CHANGED: axios -> apiClient
|
|
519
|
+
const res = await apiClient.delete(`${HEADLESS_BASE}/account/authenticators/totp`);
|
|
694
520
|
return res.data;
|
|
695
521
|
} catch (error) {
|
|
696
522
|
throw normaliseApiError(error, 'Auth.TOTP_DEACTIVATE_FAILED');
|
|
697
523
|
}
|
|
698
524
|
}
|
|
699
|
-
// -----------------------------
|
|
700
|
-
// MFA: Recovery Codes
|
|
701
|
-
// -----------------------------
|
|
702
525
|
|
|
703
526
|
export async function fetchRecoveryCodes() {
|
|
704
527
|
try {
|
|
705
|
-
const res = await
|
|
706
|
-
`${HEADLESS_BASE}/account/authenticators/recovery-codes`,
|
|
707
|
-
{ withCredentials: true },
|
|
708
|
-
);
|
|
528
|
+
const res = await apiClient.get(`${HEADLESS_BASE}/account/authenticators/recovery-codes`);
|
|
709
529
|
const body = res.data || {};
|
|
710
|
-
// Inneres "data" herausziehen, sonst direkt body verwenden
|
|
711
530
|
const data = body.data || body;
|
|
712
|
-
return data;
|
|
531
|
+
return data;
|
|
713
532
|
} catch (error) {
|
|
714
533
|
const status = error.response?.status;
|
|
715
534
|
if (status === 404) {
|
|
716
|
-
|
|
717
|
-
const resPost = await axios.post(
|
|
535
|
+
const resPost = await apiClient.post(
|
|
718
536
|
`${HEADLESS_BASE}/account/authenticators/recovery-codes`,
|
|
719
|
-
{}
|
|
720
|
-
{ withCredentials: true },
|
|
537
|
+
{}
|
|
721
538
|
);
|
|
722
539
|
const body = resPost.data || {};
|
|
723
540
|
const data = body.data || body;
|
|
@@ -729,10 +546,9 @@ export async function fetchRecoveryCodes() {
|
|
|
729
546
|
|
|
730
547
|
export async function generateRecoveryCodes() {
|
|
731
548
|
try {
|
|
732
|
-
const res = await
|
|
549
|
+
const res = await apiClient.post(
|
|
733
550
|
`${HEADLESS_BASE}/account/authenticators/recovery-codes`,
|
|
734
|
-
{}
|
|
735
|
-
{ withCredentials: true },
|
|
551
|
+
{}
|
|
736
552
|
);
|
|
737
553
|
const body = res.data || {};
|
|
738
554
|
const data = body.data || body;
|
|
@@ -742,23 +558,16 @@ export async function generateRecoveryCodes() {
|
|
|
742
558
|
}
|
|
743
559
|
}
|
|
744
560
|
|
|
745
|
-
|
|
746
|
-
|
|
747
561
|
export async function fetchOrGenerateRecoveryCodes() {
|
|
748
562
|
try {
|
|
749
|
-
const res = await
|
|
750
|
-
`${HEADLESS_BASE}/mfa/recovery_codes`,
|
|
751
|
-
{ withCredentials: true },
|
|
752
|
-
);
|
|
563
|
+
const res = await apiClient.get(`${HEADLESS_BASE}/mfa/recovery_codes`);
|
|
753
564
|
return res.data;
|
|
754
565
|
} catch (error) {
|
|
755
566
|
const { response } = error;
|
|
756
567
|
if (response?.status === 404) {
|
|
757
|
-
|
|
758
|
-
const resPost = await axios.post(
|
|
568
|
+
const resPost = await apiClient.post(
|
|
759
569
|
`${HEADLESS_BASE}/mfa/recovery_codes`,
|
|
760
|
-
{}
|
|
761
|
-
{ withCredentials: true },
|
|
570
|
+
{}
|
|
762
571
|
);
|
|
763
572
|
return resPost.data;
|
|
764
573
|
}
|
|
@@ -766,76 +575,59 @@ export async function fetchOrGenerateRecoveryCodes() {
|
|
|
766
575
|
}
|
|
767
576
|
}
|
|
768
577
|
|
|
769
|
-
|
|
770
578
|
export function isStrongSession(session) {
|
|
771
579
|
const methods = session?.methods || [];
|
|
772
580
|
const used = methods.map((m) => m.method);
|
|
773
|
-
|
|
774
|
-
// alles, was klar 2FA / starker Faktor ist:
|
|
775
581
|
const strongMethods = ['totp', 'recovery_codes', 'webauthn'];
|
|
776
|
-
|
|
777
582
|
return used.some((m) => strongMethods.includes(m));
|
|
778
583
|
}
|
|
779
584
|
|
|
780
585
|
export async function requestMfaSupportHelp(emailOrIdentifier, message = '') {
|
|
781
|
-
// Wir nutzen hier die Users-API, nicht HEADLESS_BASE
|
|
782
586
|
const payload = { email: emailOrIdentifier, message };
|
|
783
|
-
|
|
784
|
-
const res = await axios.post(
|
|
587
|
+
const res = await apiClient.post(
|
|
785
588
|
`${USERS_BASE}/mfa/support-help/`,
|
|
786
|
-
payload
|
|
787
|
-
{ withCredentials: true }
|
|
589
|
+
payload
|
|
788
590
|
);
|
|
789
591
|
return res.data;
|
|
790
592
|
}
|
|
791
593
|
|
|
792
594
|
export async function fetchRecoveryRequests(status = 'pending') {
|
|
793
|
-
const res = await
|
|
595
|
+
const res = await apiClient.get(
|
|
794
596
|
'/api/support/recovery-requests/',
|
|
795
|
-
{
|
|
796
|
-
params: { status },
|
|
797
|
-
withCredentials: true,
|
|
798
|
-
},
|
|
597
|
+
{ params: { status } }
|
|
799
598
|
);
|
|
800
599
|
return res.data;
|
|
801
600
|
}
|
|
802
601
|
|
|
803
602
|
export async function approveRecoveryRequest(id, supportNote) {
|
|
804
|
-
const res = await
|
|
603
|
+
const res = await apiClient.post(
|
|
805
604
|
`/api/support/recovery-requests/${id}/approve/`,
|
|
806
|
-
{ support_note: supportNote || '' }
|
|
807
|
-
{ withCredentials: true },
|
|
605
|
+
{ support_note: supportNote || '' }
|
|
808
606
|
);
|
|
809
607
|
return res.data;
|
|
810
608
|
}
|
|
811
609
|
|
|
812
610
|
export async function rejectRecoveryRequest(id, supportNote) {
|
|
813
|
-
const res = await
|
|
611
|
+
const res = await apiClient.post(
|
|
814
612
|
`/api/support/recovery-requests/${id}/reject/`,
|
|
815
|
-
{ support_note: supportNote || '' }
|
|
816
|
-
{ withCredentials: true },
|
|
613
|
+
{ support_note: supportNote || '' }
|
|
817
614
|
);
|
|
818
615
|
return res.data;
|
|
819
616
|
}
|
|
820
617
|
|
|
821
|
-
|
|
822
618
|
export async function loginWithRecoveryPassword(email, password, token) {
|
|
823
619
|
try {
|
|
824
|
-
await
|
|
620
|
+
await apiClient.post(
|
|
825
621
|
`/api/support/recovery-requests/recovery-login/${token}/`,
|
|
826
|
-
{ email, password }
|
|
827
|
-
{ withCredentials: true },
|
|
622
|
+
{ email, password }
|
|
828
623
|
);
|
|
829
624
|
} catch (error) {
|
|
830
625
|
throw normaliseApiError(error, 'Auth.RECOVERY_LOGIN_FAILED');
|
|
831
626
|
}
|
|
832
|
-
|
|
833
627
|
const user = await fetchCurrentUser();
|
|
834
628
|
return { user, needsMfa: false };
|
|
835
629
|
}
|
|
836
630
|
|
|
837
|
-
|
|
838
|
-
|
|
839
631
|
// -----------------------------
|
|
840
632
|
// Aggregated API object
|
|
841
633
|
// -----------------------------
|
|
@@ -872,4 +664,4 @@ export const authApi = {
|
|
|
872
664
|
loginWithRecoveryPassword,
|
|
873
665
|
};
|
|
874
666
|
|
|
875
|
-
export default authApi;
|
|
667
|
+
export default authApi;
|