@micha.bigler/ui-core-micha 1.2.4 → 1.2.6

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,61 @@
1
1
  import axios from 'axios';
2
2
  import { HEADLESS_BASE, USERS_BASE, ACCESS_CODES_BASE } from './authConfig';
3
3
  // -----------------------------
4
+ // WebAuthn Serialization Helpers
5
+ // -----------------------------
6
+ /**
7
+ * Converts an ArrayBuffer to a Base64URL encoded string.
8
+ * Required for manual credential serialization when .toJSON() is missing.
9
+ */
10
+ function bufferToBase64URL(buffer) {
11
+ const bytes = new Uint8Array(buffer);
12
+ let str = '';
13
+ for (const char of bytes) {
14
+ str += String.fromCharCode(char);
15
+ }
16
+ const base64 = btoa(str);
17
+ return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
18
+ }
19
+ /**
20
+ * Serializes a PublicKeyCredential into a JSON-compatible object.
21
+ * Acts as a polyfill for environments where credential.toJSON() is missing (e.g., Proton Pass).
22
+ */
23
+ function serializeCredential(credential) {
24
+ if (typeof credential.toJSON === 'function') {
25
+ return credential.toJSON();
26
+ }
27
+ // Manual serialization for extensions that return incomplete objects
28
+ const p = {
29
+ id: credential.id,
30
+ rawId: bufferToBase64URL(credential.rawId),
31
+ type: credential.type,
32
+ response: {
33
+ clientDataJSON: bufferToBase64URL(credential.response.clientDataJSON),
34
+ },
35
+ };
36
+ if (credential.response.attestationObject) {
37
+ // Registration specific
38
+ p.response.attestationObject = bufferToBase64URL(credential.response.attestationObject);
39
+ }
40
+ if (credential.response.authenticatorData) {
41
+ // Login specific
42
+ p.response.authenticatorData = bufferToBase64URL(credential.response.authenticatorData);
43
+ }
44
+ if (credential.response.signature) {
45
+ // Login specific
46
+ p.response.signature = bufferToBase64URL(credential.response.signature);
47
+ }
48
+ if (credential.response.userHandle) {
49
+ // Login specific
50
+ p.response.userHandle = bufferToBase64URL(credential.response.userHandle);
51
+ }
52
+ // Include clientExtensionResults if present
53
+ if (typeof credential.getClientExtensionResults === 'function') {
54
+ p.clientExtensionResults = credential.getClientExtensionResults();
55
+ }
56
+ return p;
57
+ }
58
+ // -----------------------------
4
59
  // Error helper
5
60
  // -----------------------------
6
61
  function extractErrorMessage(error) {
@@ -178,12 +233,13 @@ async function registerPasskeyStart({ passwordless = true } = {}) {
178
233
  params: passwordless ? { passwordless: true } : {},
179
234
  withCredentials: true,
180
235
  });
181
- const data = res.data || {};
182
- // allauth.headless: { creation_options: { publicKey: { ... } } }
183
- // Wir wollen am Ende den INNEREN publicKey-Block haben:
184
- const publicKeyJson = (data.creation_options && data.creation_options.publicKey) ||
185
- data.publicKey ||
186
- data;
236
+ const responseBody = res.data || {};
237
+ // Handle nested 'data' wrapper if present, otherwise use body directly
238
+ const payload = responseBody.data || responseBody;
239
+ // Extract the inner publicKey structure required by the browser API
240
+ const publicKeyJson = (payload.creation_options && payload.creation_options.publicKey) ||
241
+ payload.publicKey ||
242
+ payload;
187
243
  return publicKeyJson;
188
244
  }
189
245
  async function registerPasskeyComplete(credentialJson, name = 'Passkey') {
@@ -198,17 +254,17 @@ export async function registerPasskey(name = 'Passkey') {
198
254
  if (!hasJsonWebAuthn) {
199
255
  throw new Error('Passkey JSON helpers are not available in this browser.');
200
256
  }
201
- // Hier bekommst du bereits den inneren publicKey-Block mit challenge etc.
257
+ // ... (previous logic for registerPasskeyStart) ...
202
258
  const publicKeyJson = await registerPasskeyStart({ passwordless: true });
203
259
  let credential;
204
260
  try {
205
- // publicKeyJson hat challenge auf Top-Level
206
261
  const publicKeyOptions = window.PublicKeyCredential.parseCreationOptionsFromJSON(publicKeyJson);
207
262
  credential = await navigator.credentials.create({
208
263
  publicKey: publicKeyOptions,
209
264
  });
210
265
  }
211
266
  catch (err) {
267
+ // ... (error handling) ...
212
268
  if (err && err.name === 'NotAllowedError') {
213
269
  throw new Error('Passkey creation was cancelled by the user.');
214
270
  }
@@ -217,7 +273,8 @@ export async function registerPasskey(name = 'Passkey') {
217
273
  if (!credential) {
218
274
  throw new Error('Passkey creation was cancelled.');
219
275
  }
220
- const credentialJson = credential.toJSON();
276
+ // CHANGED: Use helper instead of direct .toJSON()
277
+ const credentialJson = serializeCredential(credential);
221
278
  return registerPasskeyComplete(credentialJson, name);
222
279
  }
223
280
  // -----------------------------
@@ -226,11 +283,13 @@ export async function registerPasskey(name = 'Passkey') {
226
283
  async function loginWithPasskeyStart() {
227
284
  ensureWebAuthnSupport();
228
285
  const res = await axios.get(`${HEADLESS_BASE}/auth/webauthn/login`, { withCredentials: true });
229
- const data = res.data || {};
230
- // allauth.headless: { request_options: { publicKey: { ... } } }
231
- const requestOptionsJson = (data.request_options && data.request_options.publicKey) ||
232
- data.request_options ||
233
- data;
286
+ const responseBody = res.data || {};
287
+ // Handle nested 'data' wrapper if present
288
+ const payload = responseBody.data || responseBody;
289
+ // Extract request options for authentication
290
+ const requestOptionsJson = (payload.request_options && payload.request_options.publicKey) ||
291
+ payload.request_options ||
292
+ payload;
234
293
  return requestOptionsJson;
235
294
  }
236
295
  async function loginWithPasskeyComplete(credentialJson) {
@@ -251,15 +310,13 @@ export async function loginWithPasskey() {
251
310
  });
252
311
  }
253
312
  catch (err) {
254
- if (err && err.name === 'NotAllowedError') {
255
- throw new Error('Passkey authentication was cancelled by the user.');
256
- }
313
+ // ... (error handling) ...
257
314
  throw err;
258
315
  }
259
316
  if (!assertion) {
260
317
  throw new Error('Passkey authentication was cancelled.');
261
318
  }
262
- const credentialJson = assertion.toJSON();
319
+ const credentialJson = serializeCredential(assertion);
263
320
  let postError = null;
264
321
  try {
265
322
  await loginWithPasskeyComplete(credentialJson);
@@ -279,6 +336,22 @@ export async function loginWithPasskey() {
279
336
  throw new Error(extractErrorMessage(err));
280
337
  }
281
338
  }
339
+ /**
340
+ * Loads all authenticators and filters for WebAuthn passkeys.
341
+ */
342
+ export async function fetchPasskeys() {
343
+ const res = await axios.get(`${HEADLESS_BASE}/account/authenticators/`, { withCredentials: true });
344
+ const items = Array.isArray(res.data) ? res.data : [];
345
+ // allauth usually returns objects like:
346
+ // { id, type, name, last_used_at, created_at, is_device_passkey, ... }
347
+ return items.filter((item) => item.type === 'webauthn');
348
+ }
349
+ /**
350
+ * Deletes a single passkey authenticator by id.
351
+ */
352
+ export async function deletePasskey(id) {
353
+ await axios.delete(`${HEADLESS_BASE}/account/authenticators/${id}/`, { withCredentials: true });
354
+ }
282
355
  // -----------------------------
283
356
  // Aggregated API object
284
357
  // -----------------------------
@@ -295,6 +368,8 @@ export const authApi = {
295
368
  setNewPassword,
296
369
  loginWithPasskey,
297
370
  registerPasskey,
371
+ fetchPasskeys,
372
+ deletePasskey,
298
373
  validateAccessCode,
299
374
  requestInviteWithCode,
300
375
  };
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useState } from 'react';
3
- import { Box, Typography, Divider, Button, Stack, Alert, } from '@mui/material';
3
+ import { Box, Typography, Divider, Button, Stack, Alert, TextField, } from '@mui/material';
4
4
  import PasswordChangeForm from './PasswordChangeForm';
5
5
  import SocialLoginButtons from './SocialLoginButtons';
6
6
  import { authApi } from '../auth/authApi';
@@ -8,12 +8,13 @@ import { FEATURES } from '../auth/authConfig';
8
8
  const SecurityComponent = () => {
9
9
  const [message, setMessage] = useState('');
10
10
  const [error, setError] = useState('');
11
- const handleSocialClick = (provider) => {
11
+ const [passkeyName, setPasskeyName] = useState('');
12
+ const [passkeySubmitting, setPasskeySubmitting] = useState(false);
13
+ const handleSocialClick = async (provider) => {
12
14
  setMessage('');
13
15
  setError('');
14
16
  try {
15
- authApi.startSocialLogin(provider);
16
- // Ab hier verlässt der Browser normalerweise die Seite
17
+ await authApi.startSocialLogin(provider);
17
18
  }
18
19
  catch (e) {
19
20
  setError(e.message || 'Social login could not be started.');
@@ -34,16 +35,26 @@ const SecurityComponent = () => {
34
35
  setError(errorMsg);
35
36
  }
36
37
  };
37
- return (_jsxs(Box, { children: [message && (_jsx(Alert, { severity: "success", sx: { mb: 2 }, children: message })), error && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error })), _jsx(Typography, { variant: "h6", gutterBottom: true, children: "Password" }), _jsx(PasswordChangeForm, { onSubmit: handlePasswordChange }), _jsx(Divider, { sx: { my: 3 } }), _jsx(Typography, { variant: "h6", gutterBottom: true, children: "Social logins" }), _jsx(Typography, { variant: "body2", sx: { mb: 1 }, children: "Sign in using a connected Google or Microsoft account." }), _jsx(SocialLoginButtons, { onProviderClick: handleSocialClick }), _jsx(Divider, { sx: { my: 3 } }), _jsx(Typography, { variant: "h6", gutterBottom: true, children: "Passkeys" }), _jsx(Typography, { variant: "body2", sx: { mb: 1 }, children: "Use passkeys for passwordless sign-in on this device." }), _jsx(Stack, { direction: "row", spacing: 2, children: _jsx(Button, { variant: "outlined", disabled: !FEATURES.passkeysEnabled, onClick: async () => {
38
- setMessage('');
39
- setError('');
40
- try {
41
- await authApi.registerPasskey('Default passkey');
42
- setMessage('Passkey added successfully.');
43
- }
44
- catch (e) {
45
- setError(e.message || 'Could not add passkey.');
46
- }
47
- }, children: FEATURES.passkeysEnabled ? 'Add passkey' : 'Passkeys disabled' }) })] }));
38
+ const handleRegisterPasskey = async () => {
39
+ setMessage('');
40
+ setError('');
41
+ setPasskeySubmitting(true);
42
+ try {
43
+ // Fallback-Name, wenn der User nichts einträgt
44
+ const fallbackName = (passkeyName === null || passkeyName === void 0 ? void 0 : passkeyName.trim()) ||
45
+ `Passkey on ${navigator.platform || 'this device'}`;
46
+ await authApi.registerPasskey(fallbackName);
47
+ setMessage('Passkey added successfully.');
48
+ setPasskeyName('');
49
+ }
50
+ catch (e) {
51
+ setError(e.message || 'Could not add passkey.');
52
+ }
53
+ finally {
54
+ setPasskeySubmitting(false);
55
+ }
56
+ };
57
+ const passkeysSupported = typeof window !== 'undefined' && !!window.PublicKeyCredential;
58
+ return (_jsxs(Box, { children: [message && (_jsx(Alert, { severity: "success", sx: { mb: 2 }, children: message })), error && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error })), _jsx(Typography, { variant: "h6", gutterBottom: true, children: "Password" }), _jsx(PasswordChangeForm, { onSubmit: handlePasswordChange }), _jsx(Divider, { sx: { my: 3 } }), _jsx(Typography, { variant: "h6", gutterBottom: true, children: "Social logins" }), _jsx(Typography, { variant: "body2", sx: { mb: 1 }, children: "Sign in using a connected Google or Microsoft account." }), _jsx(SocialLoginButtons, { onProviderClick: handleSocialClick }), _jsx(Divider, { sx: { my: 3 } }), _jsx(Typography, { variant: "h6", gutterBottom: true, children: "Passkeys" }), _jsx(Typography, { variant: "body2", sx: { mb: 1 }, children: "Use passkeys for passwordless sign-in on this device." }), !passkeysSupported && (_jsx(Typography, { variant: "body2", color: "text.secondary", sx: { mb: 1 }, children: "Passkeys are not supported in this browser." })), FEATURES.passkeysEnabled && passkeysSupported ? (_jsx(PasskeysComponent, {})) : (_jsx(Typography, { variant: "body2", color: "text.secondary", sx: { mt: 1 }, children: "Passkeys are disabled for this project." }))] }));
48
59
  };
49
60
  export default SecurityComponent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "private": false,
@@ -1,6 +1,72 @@
1
1
  import axios from 'axios';
2
2
  import { HEADLESS_BASE, USERS_BASE, ACCESS_CODES_BASE } from './authConfig';
3
3
 
4
+ // -----------------------------
5
+ // WebAuthn Serialization Helpers
6
+ // -----------------------------
7
+
8
+ /**
9
+ * Converts an ArrayBuffer to a Base64URL encoded string.
10
+ * Required for manual credential serialization when .toJSON() is missing.
11
+ */
12
+ function bufferToBase64URL(buffer) {
13
+ const bytes = new Uint8Array(buffer);
14
+ let str = '';
15
+ for (const char of bytes) {
16
+ str += String.fromCharCode(char);
17
+ }
18
+ const base64 = btoa(str);
19
+ return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
20
+ }
21
+
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
+ function serializeCredential(credential) {
27
+ if (typeof credential.toJSON === 'function') {
28
+ return credential.toJSON();
29
+ }
30
+
31
+ // Manual serialization for extensions that return incomplete objects
32
+ const p = {
33
+ id: credential.id,
34
+ rawId: bufferToBase64URL(credential.rawId),
35
+ type: credential.type,
36
+ response: {
37
+ clientDataJSON: bufferToBase64URL(credential.response.clientDataJSON),
38
+ },
39
+ };
40
+
41
+ if (credential.response.attestationObject) {
42
+ // Registration specific
43
+ p.response.attestationObject = bufferToBase64URL(credential.response.attestationObject);
44
+ }
45
+
46
+ if (credential.response.authenticatorData) {
47
+ // Login specific
48
+ p.response.authenticatorData = bufferToBase64URL(credential.response.authenticatorData);
49
+ }
50
+
51
+ if (credential.response.signature) {
52
+ // Login specific
53
+ p.response.signature = bufferToBase64URL(credential.response.signature);
54
+ }
55
+
56
+ if (credential.response.userHandle) {
57
+ // Login specific
58
+ p.response.userHandle = bufferToBase64URL(credential.response.userHandle);
59
+ }
60
+
61
+ // Include clientExtensionResults if present
62
+ if (typeof credential.getClientExtensionResults === 'function') {
63
+ p.clientExtensionResults = credential.getClientExtensionResults();
64
+ }
65
+
66
+ return p;
67
+ }
68
+
69
+
4
70
  // -----------------------------
5
71
  // Error helper
6
72
  // -----------------------------
@@ -235,18 +301,21 @@ async function registerPasskeyStart({ passwordless = true } = {}) {
235
301
  },
236
302
  );
237
303
 
238
- const data = res.data || {};
304
+ const responseBody = res.data || {};
239
305
 
240
- // allauth.headless: { creation_options: { publicKey: { ... } } }
241
- // Wir wollen am Ende den INNEREN publicKey-Block haben:
306
+ // Handle nested 'data' wrapper if present, otherwise use body directly
307
+ const payload = responseBody.data || responseBody;
308
+
309
+ // Extract the inner publicKey structure required by the browser API
242
310
  const publicKeyJson =
243
- (data.creation_options && data.creation_options.publicKey) ||
244
- data.publicKey ||
245
- data;
311
+ (payload.creation_options && payload.creation_options.publicKey) ||
312
+ payload.publicKey ||
313
+ payload;
246
314
 
247
315
  return publicKeyJson;
248
316
  }
249
317
 
318
+
250
319
  async function registerPasskeyComplete(credentialJson, name = 'Passkey') {
251
320
  const res = await axios.post(
252
321
  `${HEADLESS_BASE}/account/authenticators/webauthn`,
@@ -266,19 +335,19 @@ export async function registerPasskey(name = 'Passkey') {
266
335
  throw new Error('Passkey JSON helpers are not available in this browser.');
267
336
  }
268
337
 
269
- // Hier bekommst du bereits den inneren publicKey-Block mit challenge etc.
338
+ // ... (previous logic for registerPasskeyStart) ...
270
339
  const publicKeyJson = await registerPasskeyStart({ passwordless: true });
271
340
 
272
341
  let credential;
273
342
  try {
274
- // publicKeyJson hat challenge auf Top-Level
275
- const publicKeyOptions =
276
- window.PublicKeyCredential.parseCreationOptionsFromJSON(publicKeyJson);
343
+ const publicKeyOptions =
344
+ window.PublicKeyCredential.parseCreationOptionsFromJSON(publicKeyJson);
277
345
 
278
- credential = await navigator.credentials.create({
279
- publicKey: publicKeyOptions,
280
- });
346
+ credential = await navigator.credentials.create({
347
+ publicKey: publicKeyOptions,
348
+ });
281
349
  } catch (err) {
350
+ // ... (error handling) ...
282
351
  if (err && err.name === 'NotAllowedError') {
283
352
  throw new Error('Passkey creation was cancelled by the user.');
284
353
  }
@@ -289,7 +358,9 @@ export async function registerPasskey(name = 'Passkey') {
289
358
  throw new Error('Passkey creation was cancelled.');
290
359
  }
291
360
 
292
- const credentialJson = credential.toJSON();
361
+ // CHANGED: Use helper instead of direct .toJSON()
362
+ const credentialJson = serializeCredential(credential);
363
+
293
364
  return registerPasskeyComplete(credentialJson, name);
294
365
  }
295
366
 
@@ -304,13 +375,16 @@ async function loginWithPasskeyStart() {
304
375
  { withCredentials: true },
305
376
  );
306
377
 
307
- const data = res.data || {};
378
+ const responseBody = res.data || {};
308
379
 
309
- // allauth.headless: { request_options: { publicKey: { ... } } }
380
+ // Handle nested 'data' wrapper if present
381
+ const payload = responseBody.data || responseBody;
382
+
383
+ // Extract request options for authentication
310
384
  const requestOptionsJson =
311
- (data.request_options && data.request_options.publicKey) ||
312
- data.request_options ||
313
- data;
385
+ (payload.request_options && payload.request_options.publicKey) ||
386
+ payload.request_options ||
387
+ payload;
314
388
 
315
389
  return requestOptionsJson;
316
390
  }
@@ -342,9 +416,7 @@ export async function loginWithPasskey() {
342
416
  publicKey: publicKeyOptions,
343
417
  });
344
418
  } catch (err) {
345
- if (err && err.name === 'NotAllowedError') {
346
- throw new Error('Passkey authentication was cancelled by the user.');
347
- }
419
+ // ... (error handling) ...
348
420
  throw err;
349
421
  }
350
422
 
@@ -352,7 +424,7 @@ export async function loginWithPasskey() {
352
424
  throw new Error('Passkey authentication was cancelled.');
353
425
  }
354
426
 
355
- const credentialJson = assertion.toJSON();
427
+ const credentialJson = serializeCredential(assertion);
356
428
 
357
429
  let postError = null;
358
430
  try {
@@ -372,6 +444,30 @@ export async function loginWithPasskey() {
372
444
  throw new Error(extractErrorMessage(err));
373
445
  }
374
446
  }
447
+ /**
448
+ * Loads all authenticators and filters for WebAuthn passkeys.
449
+ */
450
+ export async function fetchPasskeys() {
451
+ const res = await axios.get(
452
+ `${HEADLESS_BASE}/account/authenticators/`,
453
+ { withCredentials: true },
454
+ );
455
+
456
+ const items = Array.isArray(res.data) ? res.data : [];
457
+ // allauth usually returns objects like:
458
+ // { id, type, name, last_used_at, created_at, is_device_passkey, ... }
459
+ return items.filter((item) => item.type === 'webauthn');
460
+ }
461
+
462
+ /**
463
+ * Deletes a single passkey authenticator by id.
464
+ */
465
+ export async function deletePasskey(id) {
466
+ await axios.delete(
467
+ `${HEADLESS_BASE}/account/authenticators/${id}/`,
468
+ { withCredentials: true },
469
+ );
470
+ }
375
471
 
376
472
  // -----------------------------
377
473
  // Aggregated API object
@@ -389,6 +485,8 @@ export const authApi = {
389
485
  setNewPassword,
390
486
  loginWithPasskey,
391
487
  registerPasskey,
488
+ fetchPasskeys,
489
+ deletePasskey,
392
490
  validateAccessCode,
393
491
  requestInviteWithCode,
394
492
  };
@@ -6,22 +6,24 @@ import {
6
6
  Button,
7
7
  Stack,
8
8
  Alert,
9
+ TextField,
9
10
  } from '@mui/material';
10
11
  import PasswordChangeForm from './PasswordChangeForm';
11
12
  import SocialLoginButtons from './SocialLoginButtons';
12
- import { authApi } from '../auth/authApi';
13
+ import { authApi } from '../auth/authApi';
13
14
  import { FEATURES } from '../auth/authConfig';
14
15
 
15
16
  const SecurityComponent = () => {
16
17
  const [message, setMessage] = useState('');
17
18
  const [error, setError] = useState('');
19
+ const [passkeyName, setPasskeyName] = useState('');
20
+ const [passkeySubmitting, setPasskeySubmitting] = useState(false);
18
21
 
19
- const handleSocialClick = (provider) => {
22
+ const handleSocialClick = async (provider) => {
20
23
  setMessage('');
21
24
  setError('');
22
25
  try {
23
- authApi.startSocialLogin(provider);
24
- // Ab hier verlässt der Browser normalerweise die Seite
26
+ await authApi.startSocialLogin(provider);
25
27
  } catch (e) {
26
28
  setError(e.message || 'Social login could not be started.');
27
29
  }
@@ -34,13 +36,38 @@ const SecurityComponent = () => {
34
36
  await authApi.changePassword(currentPassword, newPassword);
35
37
  setMessage('Password changed successfully.');
36
38
  } catch (err) {
37
- const errorMsg = err.response?.data?.detail ||
38
- err.message ||
39
- 'Could not change password.';
39
+ const errorMsg =
40
+ err.response?.data?.detail ||
41
+ err.message ||
42
+ 'Could not change password.';
40
43
  setError(errorMsg);
41
44
  }
42
45
  };
43
46
 
47
+ const handleRegisterPasskey = async () => {
48
+ setMessage('');
49
+ setError('');
50
+ setPasskeySubmitting(true);
51
+
52
+ try {
53
+ // Fallback-Name, wenn der User nichts einträgt
54
+ const fallbackName =
55
+ passkeyName?.trim() ||
56
+ `Passkey on ${navigator.platform || 'this device'}`;
57
+
58
+ await authApi.registerPasskey(fallbackName);
59
+ setMessage('Passkey added successfully.');
60
+ setPasskeyName('');
61
+ } catch (e) {
62
+ setError(e.message || 'Could not add passkey.');
63
+ } finally {
64
+ setPasskeySubmitting(false);
65
+ }
66
+ };
67
+
68
+ const passkeysSupported =
69
+ typeof window !== 'undefined' && !!window.PublicKeyCredential;
70
+
44
71
  return (
45
72
  <Box>
46
73
  {message && (
@@ -58,9 +85,8 @@ const SecurityComponent = () => {
58
85
  <Typography variant="h6" gutterBottom>
59
86
  Password
60
87
  </Typography>
61
-
62
88
  <PasswordChangeForm onSubmit={handlePasswordChange} />
63
-
89
+
64
90
  <Divider sx={{ my: 3 }} />
65
91
 
66
92
  {/* Social Logins Section */}
@@ -74,7 +100,7 @@ const SecurityComponent = () => {
74
100
 
75
101
  <Divider sx={{ my: 3 }} />
76
102
 
77
- {/* Passkeys Section (Placeholder) */}
103
+ {/* Passkeys Section */}
78
104
  <Typography variant="h6" gutterBottom>
79
105
  Passkeys
80
106
  </Typography>
@@ -82,26 +108,23 @@ const SecurityComponent = () => {
82
108
  Use passkeys for passwordless sign-in on this device.
83
109
  </Typography>
84
110
 
85
- <Stack direction="row" spacing={2}>
86
- <Button
87
- variant="outlined"
88
- disabled={!FEATURES.passkeysEnabled}
89
- onClick={async () => {
90
- setMessage('');
91
- setError('');
92
- try {
93
- await authApi.registerPasskey('Default passkey');
94
- setMessage('Passkey added successfully.');
95
- } catch (e) {
96
- setError(e.message || 'Could not add passkey.');
97
- }
98
- }}
99
- >
100
- {FEATURES.passkeysEnabled ? 'Add passkey' : 'Passkeys disabled'}
101
- </Button>
102
- </Stack>
111
+ {!passkeysSupported && (
112
+ <Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
113
+ Passkeys are not supported in this browser.
114
+ </Typography>
115
+ )}
116
+
117
+ {FEATURES.passkeysEnabled && passkeysSupported ? (
118
+ <PasskeysComponent />
119
+ ) : (
120
+ <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
121
+ Passkeys are disabled for this project.
122
+ </Typography>
123
+ )}
124
+
125
+ {/* Hier später: Liste der vorhandenen Passkeys + Delete-Buttons */}
103
126
  </Box>
104
127
  );
105
128
  };
106
129
 
107
- export default SecurityComponent;
130
+ export default SecurityComponent;