@micha.bigler/ui-core-micha 1.2.5 → 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.
- package/dist/auth/authApi.js +79 -7
- package/dist/components/SecurityComponent.js +26 -15
- package/package.json +1 -1
- package/src/auth/authApi.jsx +104 -12
- package/src/components/SecurityComponent.jsx +52 -29
package/dist/auth/authApi.js
CHANGED
|
@@ -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) {
|
|
@@ -199,17 +254,17 @@ export async function registerPasskey(name = 'Passkey') {
|
|
|
199
254
|
if (!hasJsonWebAuthn) {
|
|
200
255
|
throw new Error('Passkey JSON helpers are not available in this browser.');
|
|
201
256
|
}
|
|
202
|
-
//
|
|
257
|
+
// ... (previous logic for registerPasskeyStart) ...
|
|
203
258
|
const publicKeyJson = await registerPasskeyStart({ passwordless: true });
|
|
204
259
|
let credential;
|
|
205
260
|
try {
|
|
206
|
-
// publicKeyJson hat challenge auf Top-Level
|
|
207
261
|
const publicKeyOptions = window.PublicKeyCredential.parseCreationOptionsFromJSON(publicKeyJson);
|
|
208
262
|
credential = await navigator.credentials.create({
|
|
209
263
|
publicKey: publicKeyOptions,
|
|
210
264
|
});
|
|
211
265
|
}
|
|
212
266
|
catch (err) {
|
|
267
|
+
// ... (error handling) ...
|
|
213
268
|
if (err && err.name === 'NotAllowedError') {
|
|
214
269
|
throw new Error('Passkey creation was cancelled by the user.');
|
|
215
270
|
}
|
|
@@ -218,7 +273,8 @@ export async function registerPasskey(name = 'Passkey') {
|
|
|
218
273
|
if (!credential) {
|
|
219
274
|
throw new Error('Passkey creation was cancelled.');
|
|
220
275
|
}
|
|
221
|
-
|
|
276
|
+
// CHANGED: Use helper instead of direct .toJSON()
|
|
277
|
+
const credentialJson = serializeCredential(credential);
|
|
222
278
|
return registerPasskeyComplete(credentialJson, name);
|
|
223
279
|
}
|
|
224
280
|
// -----------------------------
|
|
@@ -254,15 +310,13 @@ export async function loginWithPasskey() {
|
|
|
254
310
|
});
|
|
255
311
|
}
|
|
256
312
|
catch (err) {
|
|
257
|
-
|
|
258
|
-
throw new Error('Passkey authentication was cancelled by the user.');
|
|
259
|
-
}
|
|
313
|
+
// ... (error handling) ...
|
|
260
314
|
throw err;
|
|
261
315
|
}
|
|
262
316
|
if (!assertion) {
|
|
263
317
|
throw new Error('Passkey authentication was cancelled.');
|
|
264
318
|
}
|
|
265
|
-
const credentialJson = assertion
|
|
319
|
+
const credentialJson = serializeCredential(assertion);
|
|
266
320
|
let postError = null;
|
|
267
321
|
try {
|
|
268
322
|
await loginWithPasskeyComplete(credentialJson);
|
|
@@ -282,6 +336,22 @@ export async function loginWithPasskey() {
|
|
|
282
336
|
throw new Error(extractErrorMessage(err));
|
|
283
337
|
}
|
|
284
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
|
+
}
|
|
285
355
|
// -----------------------------
|
|
286
356
|
// Aggregated API object
|
|
287
357
|
// -----------------------------
|
|
@@ -298,6 +368,8 @@ export const authApi = {
|
|
|
298
368
|
setNewPassword,
|
|
299
369
|
loginWithPasskey,
|
|
300
370
|
registerPasskey,
|
|
371
|
+
fetchPasskeys,
|
|
372
|
+
deletePasskey,
|
|
301
373
|
validateAccessCode,
|
|
302
374
|
requestInviteWithCode,
|
|
303
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
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
package/src/auth/authApi.jsx
CHANGED
|
@@ -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
|
// -----------------------------
|
|
@@ -269,19 +335,19 @@ export async function registerPasskey(name = 'Passkey') {
|
|
|
269
335
|
throw new Error('Passkey JSON helpers are not available in this browser.');
|
|
270
336
|
}
|
|
271
337
|
|
|
272
|
-
//
|
|
338
|
+
// ... (previous logic for registerPasskeyStart) ...
|
|
273
339
|
const publicKeyJson = await registerPasskeyStart({ passwordless: true });
|
|
274
340
|
|
|
275
341
|
let credential;
|
|
276
342
|
try {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
window.PublicKeyCredential.parseCreationOptionsFromJSON(publicKeyJson);
|
|
343
|
+
const publicKeyOptions =
|
|
344
|
+
window.PublicKeyCredential.parseCreationOptionsFromJSON(publicKeyJson);
|
|
280
345
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
346
|
+
credential = await navigator.credentials.create({
|
|
347
|
+
publicKey: publicKeyOptions,
|
|
348
|
+
});
|
|
284
349
|
} catch (err) {
|
|
350
|
+
// ... (error handling) ...
|
|
285
351
|
if (err && err.name === 'NotAllowedError') {
|
|
286
352
|
throw new Error('Passkey creation was cancelled by the user.');
|
|
287
353
|
}
|
|
@@ -292,7 +358,9 @@ export async function registerPasskey(name = 'Passkey') {
|
|
|
292
358
|
throw new Error('Passkey creation was cancelled.');
|
|
293
359
|
}
|
|
294
360
|
|
|
295
|
-
|
|
361
|
+
// CHANGED: Use helper instead of direct .toJSON()
|
|
362
|
+
const credentialJson = serializeCredential(credential);
|
|
363
|
+
|
|
296
364
|
return registerPasskeyComplete(credentialJson, name);
|
|
297
365
|
}
|
|
298
366
|
|
|
@@ -348,9 +416,7 @@ export async function loginWithPasskey() {
|
|
|
348
416
|
publicKey: publicKeyOptions,
|
|
349
417
|
});
|
|
350
418
|
} catch (err) {
|
|
351
|
-
|
|
352
|
-
throw new Error('Passkey authentication was cancelled by the user.');
|
|
353
|
-
}
|
|
419
|
+
// ... (error handling) ...
|
|
354
420
|
throw err;
|
|
355
421
|
}
|
|
356
422
|
|
|
@@ -358,7 +424,7 @@ export async function loginWithPasskey() {
|
|
|
358
424
|
throw new Error('Passkey authentication was cancelled.');
|
|
359
425
|
}
|
|
360
426
|
|
|
361
|
-
const credentialJson = assertion
|
|
427
|
+
const credentialJson = serializeCredential(assertion);
|
|
362
428
|
|
|
363
429
|
let postError = null;
|
|
364
430
|
try {
|
|
@@ -378,6 +444,30 @@ export async function loginWithPasskey() {
|
|
|
378
444
|
throw new Error(extractErrorMessage(err));
|
|
379
445
|
}
|
|
380
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
|
+
}
|
|
381
471
|
|
|
382
472
|
// -----------------------------
|
|
383
473
|
// Aggregated API object
|
|
@@ -395,6 +485,8 @@ export const authApi = {
|
|
|
395
485
|
setNewPassword,
|
|
396
486
|
loginWithPasskey,
|
|
397
487
|
registerPasskey,
|
|
488
|
+
fetchPasskeys,
|
|
489
|
+
deletePasskey,
|
|
398
490
|
validateAccessCode,
|
|
399
491
|
requestInviteWithCode,
|
|
400
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 =
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
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
|
-
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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;
|