@micha.bigler/ui-core-micha 1.2.1 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,8 @@
1
1
  import axios from 'axios';
2
2
  import { HEADLESS_BASE, USERS_BASE, ACCESS_CODES_BASE } from './authConfig';
3
- // Helper to normalise error messages from API responses
3
+ // -----------------------------
4
+ // Error helper
5
+ // -----------------------------
4
6
  function extractErrorMessage(error) {
5
7
  var _a;
6
8
  const data = (_a = error.response) === null || _a === void 0 ? void 0 : _a.data;
@@ -15,65 +17,64 @@ function extractErrorMessage(error) {
15
17
  }
16
18
  return 'An error occurred. Please try again.';
17
19
  }
18
- // Helper to get CSRF token from cookies manually
20
+ // -----------------------------
21
+ // CSRF helper
22
+ // -----------------------------
19
23
  function getCsrfToken() {
20
- if (!document.cookie)
24
+ if (typeof document === 'undefined' || !document.cookie) {
21
25
  return null;
26
+ }
22
27
  const match = document.cookie.match(/csrftoken=([^;]+)/);
23
28
  return match ? match[1] : null;
24
29
  }
30
+ // -----------------------------
31
+ // WebAuthn capability helpers
32
+ // -----------------------------
25
33
  const hasJsonWebAuthn = typeof window !== 'undefined' &&
26
- window.PublicKeyCredential &&
34
+ typeof window.PublicKeyCredential !== 'undefined' &&
27
35
  typeof window.PublicKeyCredential.parseCreationOptionsFromJSON === 'function' &&
28
36
  typeof window.PublicKeyCredential.parseRequestOptionsFromJSON === 'function';
29
37
  function ensureWebAuthnSupport() {
30
- if (!window.PublicKeyCredential || !navigator.credentials) {
38
+ if (typeof window === 'undefined' ||
39
+ typeof navigator === 'undefined' ||
40
+ !window.PublicKeyCredential ||
41
+ !navigator.credentials) {
31
42
  throw new Error('Passkeys are not supported in this browser.');
32
43
  }
33
44
  }
34
- /**
35
- * Fetches the current authenticated user from your own User API.
36
- */
45
+ // -----------------------------
46
+ // User-related helpers
47
+ // -----------------------------
37
48
  export async function fetchCurrentUser() {
38
49
  const res = await axios.get(`${USERS_BASE}/current/`, {
39
50
  withCredentials: true,
40
51
  });
41
52
  return res.data;
42
53
  }
43
- /**
44
- * Updates the user profile fields.
45
- */
46
54
  export async function updateUserProfile(data) {
47
55
  const res = await axios.patch(`${USERS_BASE}/current/`, data, {
48
56
  withCredentials: true,
49
57
  });
50
58
  return res.data;
51
59
  }
52
- /**
53
- * Logs a user in using email/password via allauth headless.
54
- */
60
+ // -----------------------------
61
+ // Authentication: password
62
+ // -----------------------------
55
63
  export async function loginWithPassword(email, password) {
56
64
  try {
57
- await axios.post(`${HEADLESS_BASE}/auth/login`, {
58
- email,
59
- password,
60
- }, { withCredentials: true });
65
+ await axios.post(`${HEADLESS_BASE}/auth/login`, { email, password }, { withCredentials: true });
61
66
  }
62
67
  catch (error) {
63
68
  if (error.response && error.response.status === 409) {
64
- // User is already logged in, continue and fetch current user
69
+ // Already logged in: continue and fetch current user
65
70
  }
66
71
  else {
67
72
  throw new Error(extractErrorMessage(error));
68
73
  }
69
74
  }
70
75
  const user = await fetchCurrentUser();
71
- // Normalise return shape so callers always get { user }
72
76
  return { user };
73
77
  }
74
- /**
75
- * Requests a password reset email via allauth headless.
76
- */
77
78
  export async function requestPasswordReset(email) {
78
79
  try {
79
80
  await axios.post(`${USERS_BASE}/reset-request/`, { email }, { withCredentials: true });
@@ -82,23 +83,15 @@ export async function requestPasswordReset(email) {
82
83
  throw new Error(extractErrorMessage(error));
83
84
  }
84
85
  }
85
- /**
86
- * Sets a new password using a reset key (from email link).
87
- */
86
+ // (Falls du es noch brauchst: klassischer allauth-Key-Flow)
88
87
  export async function resetPasswordWithKey(key, newPassword) {
89
88
  try {
90
- await axios.post(`${HEADLESS_BASE}/auth/password/reset/key`, {
91
- key,
92
- password: newPassword,
93
- }, { withCredentials: true });
89
+ await axios.post(`${HEADLESS_BASE}/auth/password/reset/key`, { key, password: newPassword }, { withCredentials: true });
94
90
  }
95
91
  catch (error) {
96
92
  throw new Error(extractErrorMessage(error));
97
93
  }
98
94
  }
99
- /**
100
- * Changes the password for an authenticated user.
101
- */
102
95
  export async function changePassword(currentPassword, newPassword) {
103
96
  try {
104
97
  await axios.post(`${HEADLESS_BASE}/account/password/change`, {
@@ -110,9 +103,9 @@ export async function changePassword(currentPassword, newPassword) {
110
103
  throw new Error(extractErrorMessage(error));
111
104
  }
112
105
  }
113
- /**
114
- * Logs the user out via allauth headless.
115
- */
106
+ // -----------------------------
107
+ // Logout / Session
108
+ // -----------------------------
116
109
  export async function logoutSession() {
117
110
  try {
118
111
  const headers = {};
@@ -127,35 +120,36 @@ export async function logoutSession() {
127
120
  }
128
121
  catch (error) {
129
122
  if (error.response && [401, 404, 410].includes(error.response.status)) {
123
+ // Session already gone, nothing to do
130
124
  return;
131
125
  }
132
126
  // eslint-disable-next-line no-console
133
127
  console.error('Logout error:', error);
134
128
  }
135
129
  }
136
- /**
137
- * Starts an OAuth social login flow for the given provider.
138
- * Provider examples: "google", "microsoft".
139
- * * FIX:
140
- * 1. Uses POST instead of GET (standard for headless init flows).
141
- * 2. Uses the correct path '/providers/{provider}/login'.
142
- */
130
+ export async function fetchHeadlessSession() {
131
+ const res = await axios.get(`${HEADLESS_BASE}/auth/session`, {
132
+ withCredentials: true,
133
+ });
134
+ return res.data;
135
+ }
136
+ // -----------------------------
137
+ // Social login
138
+ // -----------------------------
143
139
  export function startSocialLogin(provider) {
140
+ if (typeof window === 'undefined') {
141
+ throw new Error('Social login is only available in a browser environment.');
142
+ }
143
+ // Classic allauth HTML flow
144
144
  window.location.href = `/accounts/${provider}/login/?process=login`;
145
145
  }
146
- /**
147
- * Prüft, ob ein Access-Code gültig ist.
148
- * Erwartet: POST /api/access-codes/validate/ { code }
149
- * Antwort: { valid: true/false } oder 400 mit detail-Fehler.
150
- */
146
+ // -----------------------------
147
+ // Invitation / access code
148
+ // -----------------------------
151
149
  export async function validateAccessCode(code) {
152
150
  const res = await axios.post(`${ACCESS_CODES_BASE}/validate/`, { code }, { withCredentials: true });
153
- return res.data; // { valid: bool } oder Error
151
+ return res.data;
154
152
  }
155
- /**
156
- * Fordert eine Einladung mit optionalem Access-Code an.
157
- * Backend prüft den Code noch einmal serverseitig.
158
- */
159
153
  export async function requestInviteWithCode(email, accessCode) {
160
154
  const payload = { email };
161
155
  if (accessCode) {
@@ -164,15 +158,9 @@ export async function requestInviteWithCode(email, accessCode) {
164
158
  const res = await axios.post(`${USERS_BASE}/invite/`, payload, { withCredentials: true });
165
159
  return res.data;
166
160
  }
167
- /**
168
- * Loads the current session information directly from allauth headless.
169
- */
170
- export async function fetchHeadlessSession() {
171
- const res = await axios.get(`${HEADLESS_BASE}/auth/session`, {
172
- withCredentials: true,
173
- });
174
- return res.data;
175
- }
161
+ // -----------------------------
162
+ // Custom password-reset via uid/token
163
+ // -----------------------------
176
164
  export async function verifyResetToken(uid, token) {
177
165
  const res = await axios.get(`${USERS_BASE}/password-reset/${uid}/${token}/`, { withCredentials: true });
178
166
  return res.data;
@@ -181,16 +169,22 @@ export async function setNewPassword(uid, token, newPassword) {
181
169
  const res = await axios.post(`${USERS_BASE}/password-reset/${uid}/${token}/`, { new_password: newPassword }, { withCredentials: true });
182
170
  return res.data;
183
171
  }
184
- // Start: Optionen für Credential-Erzeugung holen
172
+ // -----------------------------
173
+ // WebAuthn / Passkeys: register
174
+ // -----------------------------
185
175
  async function registerPasskeyStart({ passwordless = true } = {}) {
186
176
  ensureWebAuthnSupport();
187
177
  const res = await axios.get(`${HEADLESS_BASE}/account/authenticators/webauthn`, {
188
178
  params: passwordless ? { passwordless: true } : {},
189
179
  withCredentials: true,
190
180
  });
191
- return res.data; // JSON-Options vom Server
181
+ const data = res.data || {};
182
+ // allauth.headless: { creation_options: { publicKey: { ... } } }
183
+ const creationOptionsJson = (data.creation_options && data.creation_options.publicKey) ||
184
+ data.creation_options ||
185
+ data;
186
+ return creationOptionsJson;
192
187
  }
193
- // Complete: Credential an Server schicken
194
188
  async function registerPasskeyComplete(credentialJson, name = 'Passkey') {
195
189
  const res = await axios.post(`${HEADLESS_BASE}/account/authenticators/webauthn`, {
196
190
  credential: credentialJson,
@@ -198,53 +192,93 @@ async function registerPasskeyComplete(credentialJson, name = 'Passkey') {
198
192
  }, { withCredentials: true });
199
193
  return res.data;
200
194
  }
201
- // High-level Helper, den das UI aufruft
202
195
  export async function registerPasskey(name = 'Passkey') {
203
196
  ensureWebAuthnSupport();
204
- const optionsJson = await registerPasskeyStart({ passwordless: true });
205
197
  if (!hasJsonWebAuthn) {
206
- throw new Error('Passkey JSON helpers not available in this browser.');
198
+ throw new Error('Passkey JSON helpers are not available in this browser.');
199
+ }
200
+ const creationOptionsJson = await registerPasskeyStart({ passwordless: true });
201
+ let credential;
202
+ try {
203
+ const publicKeyOptions = window.PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJson);
204
+ credential = await navigator.credentials.create({
205
+ publicKey: publicKeyOptions,
206
+ });
207
+ }
208
+ catch (err) {
209
+ if (err && err.name === 'NotAllowedError') {
210
+ throw new Error('Passkey creation was cancelled by the user.');
211
+ }
212
+ throw err;
207
213
  }
208
- const publicKeyOptions = window.PublicKeyCredential.parseCreationOptionsFromJSON(optionsJson);
209
- const credential = await navigator.credentials.create({
210
- publicKey: publicKeyOptions,
211
- });
212
214
  if (!credential) {
213
215
  throw new Error('Passkey creation was cancelled.');
214
216
  }
215
217
  const credentialJson = credential.toJSON();
216
218
  return registerPasskeyComplete(credentialJson, name);
217
219
  }
220
+ // -----------------------------
221
+ // WebAuthn / Passkeys: login
222
+ // -----------------------------
218
223
  async function loginWithPasskeyStart() {
219
224
  ensureWebAuthnSupport();
220
225
  const res = await axios.get(`${HEADLESS_BASE}/auth/webauthn/login`, { withCredentials: true });
221
- return res.data;
226
+ const data = res.data || {};
227
+ // allauth.headless: { request_options: { publicKey: { ... } } }
228
+ const requestOptionsJson = (data.request_options && data.request_options.publicKey) ||
229
+ data.request_options ||
230
+ data;
231
+ return requestOptionsJson;
222
232
  }
223
233
  async function loginWithPasskeyComplete(credentialJson) {
224
234
  const res = await axios.post(`${HEADLESS_BASE}/auth/webauthn/login`, { credential: credentialJson }, { withCredentials: true });
225
235
  return res.data;
226
236
  }
227
- // Public API, die du im UI verwendest
228
237
  export async function loginWithPasskey() {
229
238
  ensureWebAuthnSupport();
230
- const optionsJson = await loginWithPasskeyStart();
231
239
  if (!hasJsonWebAuthn) {
232
- throw new Error('Passkey JSON helpers not available in this browser.');
240
+ throw new Error('Passkey JSON helpers are not available in this browser.');
241
+ }
242
+ const requestOptionsJson = await loginWithPasskeyStart();
243
+ let assertion;
244
+ try {
245
+ const publicKeyOptions = window.PublicKeyCredential.parseRequestOptionsFromJSON(requestOptionsJson);
246
+ assertion = await navigator.credentials.get({
247
+ publicKey: publicKeyOptions,
248
+ });
249
+ }
250
+ catch (err) {
251
+ if (err && err.name === 'NotAllowedError') {
252
+ throw new Error('Passkey authentication was cancelled by the user.');
253
+ }
254
+ throw err;
233
255
  }
234
- const publicKeyOptions = window.PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson);
235
- const assertion = await navigator.credentials.get({
236
- publicKey: publicKeyOptions,
237
- });
238
256
  if (!assertion) {
239
257
  throw new Error('Passkey authentication was cancelled.');
240
258
  }
241
259
  const credentialJson = assertion.toJSON();
242
- // Allauth kann hier 200 (eingeloggt) oder 401 (z.B. Email noch nicht verifiziert) liefern :contentReference[oaicite:5]{index=5}
243
- await loginWithPasskeyComplete(credentialJson);
244
- // Danach wie bei Passwort-Login aktuellen User laden
245
- const user = await fetchCurrentUser();
246
- return user;
260
+ let postError = null;
261
+ try {
262
+ await loginWithPasskeyComplete(credentialJson);
263
+ }
264
+ catch (err) {
265
+ // For example 401 although credential looked fine
266
+ postError = err;
267
+ }
268
+ try {
269
+ const user = await fetchCurrentUser();
270
+ return { user };
271
+ }
272
+ catch (err) {
273
+ if (postError) {
274
+ throw new Error(extractErrorMessage(postError));
275
+ }
276
+ throw new Error(extractErrorMessage(err));
277
+ }
247
278
  }
279
+ // -----------------------------
280
+ // Aggregated API object
281
+ // -----------------------------
248
282
  export const authApi = {
249
283
  fetchCurrentUser,
250
284
  updateUserProfile,
@@ -261,3 +295,4 @@ export const authApi = {
261
295
  validateAccessCode,
262
296
  requestInviteWithCode,
263
297
  };
298
+ export default authApi;
@@ -4,15 +4,16 @@ import { Box, Typography, Divider, Button, Stack, Alert, } from '@mui/material';
4
4
  import PasswordChangeForm from './PasswordChangeForm';
5
5
  import SocialLoginButtons from './SocialLoginButtons';
6
6
  import { authApi } from '../auth/authApi';
7
+ import { FEATURES } from '../auth/authConfig';
7
8
  const SecurityComponent = () => {
8
9
  const [message, setMessage] = useState('');
9
10
  const [error, setError] = useState('');
10
- const handleSocialClick = async (provider) => {
11
+ const handleSocialClick = (provider) => {
11
12
  setMessage('');
12
13
  setError('');
13
14
  try {
14
- // FIX: Await the async function to catch network errors
15
- await authApi.startSocialLogin(provider);
15
+ authApi.startSocialLogin(provider);
16
+ // Ab hier verlässt der Browser normalerweise die Seite
16
17
  }
17
18
  catch (e) {
18
19
  setError(e.message || 'Social login could not be started.');
@@ -40,7 +40,6 @@ export function LoginPage() {
40
40
  authApi.startSocialLogin(providerKey);
41
41
  }
42
42
  catch (err) {
43
- // eslint-disable-next-line no-console
44
43
  console.error('Social login init failed', err);
45
44
  setError('Could not start social login.');
46
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "private": false,
@@ -1,7 +1,9 @@
1
1
  import axios from 'axios';
2
- import { HEADLESS_BASE, USERS_BASE, ACCESS_CODES_BASE } from './authConfig';
2
+ import { HEADLESS_BASE, USERS_BASE, ACCESS_CODES_BASE } from './authConfig';
3
3
 
4
- // Helper to normalise error messages from API responses
4
+ // -----------------------------
5
+ // Error helper
6
+ // -----------------------------
5
7
  function extractErrorMessage(error) {
6
8
  const data = error.response?.data;
7
9
  if (!data) {
@@ -16,28 +18,40 @@ function extractErrorMessage(error) {
16
18
  return 'An error occurred. Please try again.';
17
19
  }
18
20
 
19
- // Helper to get CSRF token from cookies manually
21
+ // -----------------------------
22
+ // CSRF helper
23
+ // -----------------------------
20
24
  function getCsrfToken() {
21
- if (!document.cookie) return null;
25
+ if (typeof document === 'undefined' || !document.cookie) {
26
+ return null;
27
+ }
22
28
  const match = document.cookie.match(/csrftoken=([^;]+)/);
23
29
  return match ? match[1] : null;
24
30
  }
25
31
 
32
+ // -----------------------------
33
+ // WebAuthn capability helpers
34
+ // -----------------------------
26
35
  const hasJsonWebAuthn =
27
36
  typeof window !== 'undefined' &&
28
- window.PublicKeyCredential &&
37
+ typeof window.PublicKeyCredential !== 'undefined' &&
29
38
  typeof window.PublicKeyCredential.parseCreationOptionsFromJSON === 'function' &&
30
39
  typeof window.PublicKeyCredential.parseRequestOptionsFromJSON === 'function';
31
40
 
32
41
  function ensureWebAuthnSupport() {
33
- if (!window.PublicKeyCredential || !navigator.credentials) {
42
+ if (
43
+ typeof window === 'undefined' ||
44
+ typeof navigator === 'undefined' ||
45
+ !window.PublicKeyCredential ||
46
+ !navigator.credentials
47
+ ) {
34
48
  throw new Error('Passkeys are not supported in this browser.');
35
49
  }
36
50
  }
37
51
 
38
- /**
39
- * Fetches the current authenticated user from your own User API.
40
- */
52
+ // -----------------------------
53
+ // User-related helpers
54
+ // -----------------------------
41
55
  export async function fetchCurrentUser() {
42
56
  const res = await axios.get(`${USERS_BASE}/current/`, {
43
57
  withCredentials: true,
@@ -45,9 +59,6 @@ export async function fetchCurrentUser() {
45
59
  return res.data;
46
60
  }
47
61
 
48
- /**
49
- * Updates the user profile fields.
50
- */
51
62
  export async function updateUserProfile(data) {
52
63
  const res = await axios.patch(`${USERS_BASE}/current/`, data, {
53
64
  withCredentials: true,
@@ -55,35 +66,28 @@ export async function updateUserProfile(data) {
55
66
  return res.data;
56
67
  }
57
68
 
58
- /**
59
- * Logs a user in using email/password via allauth headless.
60
- */
69
+ // -----------------------------
70
+ // Authentication: password
71
+ // -----------------------------
61
72
  export async function loginWithPassword(email, password) {
62
73
  try {
63
74
  await axios.post(
64
75
  `${HEADLESS_BASE}/auth/login`,
65
- {
66
- email,
67
- password,
68
- },
76
+ { email, password },
69
77
  { withCredentials: true },
70
78
  );
71
79
  } catch (error) {
72
80
  if (error.response && error.response.status === 409) {
73
- // User is already logged in, continue and fetch current user
81
+ // Already logged in: continue and fetch current user
74
82
  } else {
75
83
  throw new Error(extractErrorMessage(error));
76
84
  }
77
85
  }
78
86
 
79
87
  const user = await fetchCurrentUser();
80
- // Normalise return shape so callers always get { user }
81
88
  return { user };
82
89
  }
83
90
 
84
- /**
85
- * Requests a password reset email via allauth headless.
86
- */
87
91
  export async function requestPasswordReset(email) {
88
92
  try {
89
93
  await axios.post(
@@ -96,17 +100,12 @@ export async function requestPasswordReset(email) {
96
100
  }
97
101
  }
98
102
 
99
- /**
100
- * Sets a new password using a reset key (from email link).
101
- */
103
+ // (Falls du es noch brauchst: klassischer allauth-Key-Flow)
102
104
  export async function resetPasswordWithKey(key, newPassword) {
103
105
  try {
104
106
  await axios.post(
105
107
  `${HEADLESS_BASE}/auth/password/reset/key`,
106
- {
107
- key,
108
- password: newPassword,
109
- },
108
+ { key, password: newPassword },
110
109
  { withCredentials: true },
111
110
  );
112
111
  } catch (error) {
@@ -114,9 +113,6 @@ export async function resetPasswordWithKey(key, newPassword) {
114
113
  }
115
114
  }
116
115
 
117
- /**
118
- * Changes the password for an authenticated user.
119
- */
120
116
  export async function changePassword(currentPassword, newPassword) {
121
117
  try {
122
118
  await axios.post(
@@ -132,9 +128,9 @@ export async function changePassword(currentPassword, newPassword) {
132
128
  }
133
129
  }
134
130
 
135
- /**
136
- * Logs the user out via allauth headless.
137
- */
131
+ // -----------------------------
132
+ // Logout / Session
133
+ // -----------------------------
138
134
  export async function logoutSession() {
139
135
  try {
140
136
  const headers = {};
@@ -145,13 +141,14 @@ export async function logoutSession() {
145
141
 
146
142
  await axios.delete(
147
143
  `${HEADLESS_BASE}/auth/session`,
148
- {
144
+ {
149
145
  withCredentials: true,
150
- headers,
146
+ headers,
151
147
  },
152
148
  );
153
149
  } catch (error) {
154
150
  if (error.response && [401, 404, 410].includes(error.response.status)) {
151
+ // Session already gone, nothing to do
155
152
  return;
156
153
  }
157
154
  // eslint-disable-next-line no-console
@@ -159,35 +156,37 @@ export async function logoutSession() {
159
156
  }
160
157
  }
161
158
 
162
- /**
163
- * Starts an OAuth social login flow for the given provider.
164
- * Provider examples: "google", "microsoft".
165
- * * FIX:
166
- * 1. Uses POST instead of GET (standard for headless init flows).
167
- * 2. Uses the correct path '/providers/{provider}/login'.
168
- */
159
+ export async function fetchHeadlessSession() {
160
+ const res = await axios.get(`${HEADLESS_BASE}/auth/session`, {
161
+ withCredentials: true,
162
+ });
163
+ return res.data;
164
+ }
165
+
166
+ // -----------------------------
167
+ // Social login
168
+ // -----------------------------
169
169
  export function startSocialLogin(provider) {
170
+ if (typeof window === 'undefined') {
171
+ throw new Error('Social login is only available in a browser environment.');
172
+ }
173
+
174
+ // Classic allauth HTML flow
170
175
  window.location.href = `/accounts/${provider}/login/?process=login`;
171
176
  }
172
177
 
173
- /**
174
- * Prüft, ob ein Access-Code gültig ist.
175
- * Erwartet: POST /api/access-codes/validate/ { code }
176
- * Antwort: { valid: true/false } oder 400 mit detail-Fehler.
177
- */
178
+ // -----------------------------
179
+ // Invitation / access code
180
+ // -----------------------------
178
181
  export async function validateAccessCode(code) {
179
182
  const res = await axios.post(
180
183
  `${ACCESS_CODES_BASE}/validate/`,
181
184
  { code },
182
185
  { withCredentials: true },
183
186
  );
184
- return res.data; // { valid: bool } oder Error
187
+ return res.data;
185
188
  }
186
189
 
187
- /**
188
- * Fordert eine Einladung mit optionalem Access-Code an.
189
- * Backend prüft den Code noch einmal serverseitig.
190
- */
191
190
  export async function requestInviteWithCode(email, accessCode) {
192
191
  const payload = { email };
193
192
  if (accessCode) {
@@ -202,19 +201,9 @@ export async function requestInviteWithCode(email, accessCode) {
202
201
  return res.data;
203
202
  }
204
203
 
205
-
206
- /**
207
- * Loads the current session information directly from allauth headless.
208
- */
209
- export async function fetchHeadlessSession() {
210
- const res = await axios.get(`${HEADLESS_BASE}/auth/session`, {
211
- withCredentials: true,
212
- });
213
- return res.data;
214
- }
215
-
216
-
217
-
204
+ // -----------------------------
205
+ // Custom password-reset via uid/token
206
+ // -----------------------------
218
207
  export async function verifyResetToken(uid, token) {
219
208
  const res = await axios.get(
220
209
  `${USERS_BASE}/password-reset/${uid}/${token}/`,
@@ -232,7 +221,9 @@ export async function setNewPassword(uid, token, newPassword) {
232
221
  return res.data;
233
222
  }
234
223
 
235
- // Start: Optionen für Credential-Erzeugung holen
224
+ // -----------------------------
225
+ // WebAuthn / Passkeys: register
226
+ // -----------------------------
236
227
  async function registerPasskeyStart({ passwordless = true } = {}) {
237
228
  ensureWebAuthnSupport();
238
229
 
@@ -243,10 +234,18 @@ async function registerPasskeyStart({ passwordless = true } = {}) {
243
234
  withCredentials: true,
244
235
  },
245
236
  );
246
- return res.data; // JSON-Options vom Server
237
+
238
+ const data = res.data || {};
239
+
240
+ // allauth.headless: { creation_options: { publicKey: { ... } } }
241
+ const creationOptionsJson =
242
+ (data.creation_options && data.creation_options.publicKey) ||
243
+ data.creation_options ||
244
+ data;
245
+
246
+ return creationOptionsJson;
247
247
  }
248
248
 
249
- // Complete: Credential an Server schicken
250
249
  async function registerPasskeyComplete(credentialJson, name = 'Passkey') {
251
250
  const res = await axios.post(
252
251
  `${HEADLESS_BASE}/account/authenticators/webauthn`,
@@ -259,22 +258,29 @@ async function registerPasskeyComplete(credentialJson, name = 'Passkey') {
259
258
  return res.data;
260
259
  }
261
260
 
262
- // High-level Helper, den das UI aufruft
263
261
  export async function registerPasskey(name = 'Passkey') {
264
262
  ensureWebAuthnSupport();
265
263
 
266
- const optionsJson = await registerPasskeyStart({ passwordless: true });
267
-
268
264
  if (!hasJsonWebAuthn) {
269
- throw new Error('Passkey JSON helpers not available in this browser.');
265
+ throw new Error('Passkey JSON helpers are not available in this browser.');
270
266
  }
271
267
 
272
- const publicKeyOptions =
273
- window.PublicKeyCredential.parseCreationOptionsFromJSON(optionsJson);
268
+ const creationOptionsJson = await registerPasskeyStart({ passwordless: true });
274
269
 
275
- const credential = await navigator.credentials.create({
276
- publicKey: publicKeyOptions,
277
- });
270
+ let credential;
271
+ try {
272
+ const publicKeyOptions =
273
+ window.PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJson);
274
+
275
+ credential = await navigator.credentials.create({
276
+ publicKey: publicKeyOptions,
277
+ });
278
+ } catch (err) {
279
+ if (err && err.name === 'NotAllowedError') {
280
+ throw new Error('Passkey creation was cancelled by the user.');
281
+ }
282
+ throw err;
283
+ }
278
284
 
279
285
  if (!credential) {
280
286
  throw new Error('Passkey creation was cancelled.');
@@ -284,6 +290,9 @@ export async function registerPasskey(name = 'Passkey') {
284
290
  return registerPasskeyComplete(credentialJson, name);
285
291
  }
286
292
 
293
+ // -----------------------------
294
+ // WebAuthn / Passkeys: login
295
+ // -----------------------------
287
296
  async function loginWithPasskeyStart() {
288
297
  ensureWebAuthnSupport();
289
298
 
@@ -291,7 +300,16 @@ async function loginWithPasskeyStart() {
291
300
  `${HEADLESS_BASE}/auth/webauthn/login`,
292
301
  { withCredentials: true },
293
302
  );
294
- return res.data;
303
+
304
+ const data = res.data || {};
305
+
306
+ // allauth.headless: { request_options: { publicKey: { ... } } }
307
+ const requestOptionsJson =
308
+ (data.request_options && data.request_options.publicKey) ||
309
+ data.request_options ||
310
+ data;
311
+
312
+ return requestOptionsJson;
295
313
  }
296
314
 
297
315
  async function loginWithPasskeyComplete(credentialJson) {
@@ -303,22 +321,29 @@ async function loginWithPasskeyComplete(credentialJson) {
303
321
  return res.data;
304
322
  }
305
323
 
306
- // Public API, die du im UI verwendest
307
324
  export async function loginWithPasskey() {
308
325
  ensureWebAuthnSupport();
309
326
 
310
- const optionsJson = await loginWithPasskeyStart();
311
-
312
327
  if (!hasJsonWebAuthn) {
313
- throw new Error('Passkey JSON helpers not available in this browser.');
328
+ throw new Error('Passkey JSON helpers are not available in this browser.');
314
329
  }
315
330
 
316
- const publicKeyOptions =
317
- window.PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson);
331
+ const requestOptionsJson = await loginWithPasskeyStart();
318
332
 
319
- const assertion = await navigator.credentials.get({
320
- publicKey: publicKeyOptions,
321
- });
333
+ let assertion;
334
+ try {
335
+ const publicKeyOptions =
336
+ window.PublicKeyCredential.parseRequestOptionsFromJSON(requestOptionsJson);
337
+
338
+ assertion = await navigator.credentials.get({
339
+ publicKey: publicKeyOptions,
340
+ });
341
+ } catch (err) {
342
+ if (err && err.name === 'NotAllowedError') {
343
+ throw new Error('Passkey authentication was cancelled by the user.');
344
+ }
345
+ throw err;
346
+ }
322
347
 
323
348
  if (!assertion) {
324
349
  throw new Error('Passkey authentication was cancelled.');
@@ -326,15 +351,28 @@ export async function loginWithPasskey() {
326
351
 
327
352
  const credentialJson = assertion.toJSON();
328
353
 
329
- // Allauth kann hier 200 (eingeloggt) oder 401 (z.B. Email noch nicht verifiziert) liefern :contentReference[oaicite:5]{index=5}
330
- await loginWithPasskeyComplete(credentialJson);
354
+ let postError = null;
355
+ try {
356
+ await loginWithPasskeyComplete(credentialJson);
357
+ } catch (err) {
358
+ // For example 401 although credential looked fine
359
+ postError = err;
360
+ }
331
361
 
332
- // Danach wie bei Passwort-Login aktuellen User laden
333
- const user = await fetchCurrentUser();
334
- return user;
362
+ try {
363
+ const user = await fetchCurrentUser();
364
+ return { user };
365
+ } catch (err) {
366
+ if (postError) {
367
+ throw new Error(extractErrorMessage(postError));
368
+ }
369
+ throw new Error(extractErrorMessage(err));
370
+ }
335
371
  }
336
372
 
337
-
373
+ // -----------------------------
374
+ // Aggregated API object
375
+ // -----------------------------
338
376
  export const authApi = {
339
377
  fetchCurrentUser,
340
378
  updateUserProfile,
@@ -350,4 +388,6 @@ export const authApi = {
350
388
  registerPasskey,
351
389
  validateAccessCode,
352
390
  requestInviteWithCode,
353
- };
391
+ };
392
+
393
+ export default authApi;
@@ -10,17 +10,18 @@ import {
10
10
  import PasswordChangeForm from './PasswordChangeForm';
11
11
  import SocialLoginButtons from './SocialLoginButtons';
12
12
  import { authApi } from '../auth/authApi';
13
+ import { FEATURES } from '../auth/authConfig';
13
14
 
14
15
  const SecurityComponent = () => {
15
16
  const [message, setMessage] = useState('');
16
17
  const [error, setError] = useState('');
17
18
 
18
- const handleSocialClick = async (provider) => {
19
+ const handleSocialClick = (provider) => {
19
20
  setMessage('');
20
21
  setError('');
21
22
  try {
22
- // FIX: Await the async function to catch network errors
23
- await authApi.startSocialLogin(provider);
23
+ authApi.startSocialLogin(provider);
24
+ // Ab hier verlässt der Browser normalerweise die Seite
24
25
  } catch (e) {
25
26
  setError(e.message || 'Social login could not be started.');
26
27
  }
@@ -41,7 +41,6 @@ export function LoginPage() {
41
41
  try {
42
42
  authApi.startSocialLogin(providerKey);
43
43
  } catch (err) {
44
- // eslint-disable-next-line no-console
45
44
  console.error('Social login init failed', err);
46
45
  setError('Could not start social login.');
47
46
  }