@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.
- package/dist/auth/authApi.js +93 -18
- package/dist/components/SecurityComponent.js +26 -15
- package/package.json +1 -1
- package/src/auth/authApi.jsx +121 -23
- 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) {
|
|
@@ -178,12 +233,13 @@ async function registerPasskeyStart({ passwordless = true } = {}) {
|
|
|
178
233
|
params: passwordless ? { passwordless: true } : {},
|
|
179
234
|
withCredentials: true,
|
|
180
235
|
});
|
|
181
|
-
const
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
230
|
-
//
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
// -----------------------------
|
|
@@ -235,18 +301,21 @@ async function registerPasskeyStart({ passwordless = true } = {}) {
|
|
|
235
301
|
},
|
|
236
302
|
);
|
|
237
303
|
|
|
238
|
-
const
|
|
304
|
+
const responseBody = res.data || {};
|
|
239
305
|
|
|
240
|
-
//
|
|
241
|
-
|
|
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
|
-
(
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
//
|
|
338
|
+
// ... (previous logic for registerPasskeyStart) ...
|
|
270
339
|
const publicKeyJson = await registerPasskeyStart({ passwordless: true });
|
|
271
340
|
|
|
272
341
|
let credential;
|
|
273
342
|
try {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
window.PublicKeyCredential.parseCreationOptionsFromJSON(publicKeyJson);
|
|
343
|
+
const publicKeyOptions =
|
|
344
|
+
window.PublicKeyCredential.parseCreationOptionsFromJSON(publicKeyJson);
|
|
277
345
|
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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
|
|
378
|
+
const responseBody = res.data || {};
|
|
308
379
|
|
|
309
|
-
//
|
|
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
|
-
(
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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;
|