@micha.bigler/ui-core-micha 1.4.20 → 1.4.23

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.
Files changed (33) hide show
  1. package/dist/auth/AuthContext.js +0 -1
  2. package/dist/auth/authApi.js +137 -378
  3. package/dist/components/MFAComponent.js +7 -7
  4. package/dist/components/MfaLoginComponent.js +6 -5
  5. package/dist/components/PasskeysComponent.js +5 -5
  6. package/dist/components/SecurityComponent.js +4 -3
  7. package/dist/components/SupportRecoveryRequestsTab.js +4 -4
  8. package/dist/components/UserInviteComponent.js +38 -0
  9. package/dist/components/UserListComponent.js +83 -0
  10. package/dist/pages/AccountPage.js +67 -23
  11. package/dist/pages/LoginPage.js +6 -5
  12. package/dist/pages/PasswordInvitePage.js +3 -3
  13. package/dist/pages/SignUpPage.js +3 -3
  14. package/dist/utils/authService.js +53 -0
  15. package/dist/utils/errors.js +33 -0
  16. package/dist/utils/webauthn.js +44 -0
  17. package/package.json +1 -1
  18. package/src/auth/AuthContext.jsx +0 -1
  19. package/src/auth/authApi.jsx +143 -478
  20. package/src/components/MFAComponent.jsx +7 -7
  21. package/src/components/MfaLoginComponent.jsx +6 -5
  22. package/src/components/PasskeysComponent.jsx +5 -5
  23. package/src/components/SecurityComponent.jsx +4 -3
  24. package/src/components/SupportRecoveryRequestsTab.jsx +4 -4
  25. package/src/components/UserInviteComponent.jsx +69 -0
  26. package/src/components/UserListComponent.jsx +167 -0
  27. package/src/pages/AccountPage.jsx +140 -47
  28. package/src/pages/LoginPage.jsx +6 -5
  29. package/src/pages/PasswordInvitePage.jsx +3 -3
  30. package/src/pages/SignUpPage.jsx +3 -3
  31. package/src/utils/authService.js +68 -0
  32. package/src/utils/errors.js +39 -0
  33. package/src/utils/webauthn.js +51 -0
@@ -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
- // WebAuthn capability helpers
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
- // Authentication: password
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 resetPasswordWithKey(key, newPassword) {
35
+ export async function logoutSession() {
162
36
  try {
163
- await apiClient.post(
164
- `${HEADLESS_BASE}/auth/password/reset/key`,
165
- { key, password: newPassword }
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
- export async function changePassword(currentPassword, newPassword) {
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
- throw normaliseApiError(error, 'Auth.PASSWORD_CHANGE_FAILED');
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
- // Logout / Session
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
- // CHANGED: axios -> apiClient
198
- await apiClient.delete(
199
- `${HEADLESS_BASE}/auth/session`,
200
- { headers }
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
- if (error.response && [401, 404, 410].includes(error.response.status)) {
204
- return;
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
- // eslint-disable-next-line no-console
207
- console.error('Logout error:', error);
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 fetchHeadlessSession() {
212
- const res = await apiClient.get(`${HEADLESS_BASE}/auth/session`);
213
- return res.data;
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
- // Social login
218
- // -----------------------------
219
- export function startSocialLogin(provider) {
220
- if (typeof window === 'undefined') {
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
- // Invitation / access code
95
+ // Password Management
231
96
  // -----------------------------
232
- export async function validateAccessCode(code) {
97
+
98
+ export async function requestPasswordReset(email) {
233
99
  try {
234
- const res = await apiClient.post(
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.ACCESS_CODE_INVALID_OR_INACTIVE');
102
+ throw normaliseApiError(error, 'Auth.RESET_REQUEST_FAILED');
241
103
  }
242
104
  }
243
105
 
244
- export async function requestInviteWithCode(email, accessCode) {
245
- const payload = { email };
246
- if (accessCode) {
247
- payload.access_code = accessCode;
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
- const res = await apiClient.post(
252
- `${USERS_BASE}/invite/`,
253
- payload
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.INVITE_FAILED');
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 registerPasskeyComplete(credentialJson, name = 'Passkey') {
308
- const res = await apiClient.post(
309
- `${HEADLESS_BASE}/account/authenticators/webauthn`,
310
- { credential: credentialJson, name }
311
- );
312
- return res.data;
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 registerPasskey(name = 'Passkey') {
316
- ensureWebAuthnSupport();
317
- if (!hasJsonWebAuthn) {
318
- throw normaliseApiError(
319
- new Error('Auth.PASSKEY_JSON_HELPERS_UNAVAILABLE'),
320
- 'Auth.PASSKEY_JSON_HELPERS_UNAVAILABLE'
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 loginWithPasskeyStart() {
342
- ensureWebAuthnSupport();
165
+ export async function getPasskeyLoginOptions() {
343
166
  const res = await apiClient.get(`${HEADLESS_BASE}/auth/webauthn/login`);
344
- const responseBody = res.data || {};
345
- const payload = responseBody.data || responseBody;
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 loginWithPasskeyComplete(credentialJson) {
354
- const res = await apiClient.post(
355
- `${HEADLESS_BASE}/auth/webauthn/login`,
356
- { credential: credentialJson }
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
- // Authentication: MFA Step
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
- const items = Array.isArray(body.data) ? body.data : (Array.isArray(body) ? body : []);
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 body = res.data || {};
484
- const data = body.data || body;
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
- const { response } = error;
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
- const res = await apiClient.post(
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
- // CHANGED: axios -> apiClient
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
- const body = res.data || {};
529
- const data = body.data || body;
530
- return data;
244
+ return res.data?.data || res.data;
531
245
  } catch (error) {
532
- const status = error.response?.status;
533
- if (status === 404) {
534
- const resPost = await apiClient.post(
535
- `${HEADLESS_BASE}/account/authenticators/recovery-codes`,
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
- `${HEADLESS_BASE}/account/authenticators/recovery-codes`,
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
- export async function fetchOrGenerateRecoveryCodes() {
264
+ // -----------------------------
265
+ // Invitations & Access Codes
266
+ // -----------------------------
267
+
268
+ export async function validateAccessCode(code) {
561
269
  try {
562
- const res = await apiClient.get(`${HEADLESS_BASE}/mfa/recovery_codes`);
270
+ const res = await apiClient.post(`${ACCESS_CODES_BASE}/validate/`, { code });
563
271
  return res.data;
564
272
  } catch (error) {
565
- const { response } = error;
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 isStrongSession(session) {
578
- const methods = session?.methods || [];
579
- const used = methods.map((m) => m.method);
580
- const strongMethods = ['totp', 'recovery_codes', 'webauthn'];
581
- return used.some((m) => strongMethods.includes(m));
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 payload = { email: emailOrIdentifier, message };
586
- const res = await apiClient.post(
587
- `${USERS_BASE}/mfa/support-help/`,
588
- payload
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
- `/api/support/recovery-requests/${id}/approve/`,
604
- { support_note: supportNote || '' }
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
- `/api/support/recovery-requests/${id}/reject/`,
612
- { support_note: supportNote || '' }
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
- `/api/support/recovery-requests/recovery-login/${token}/`,
621
- { email, password }
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
+ }