@micha.bigler/ui-core-micha 1.2.6 → 1.2.8
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.
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useEffect, useState } from 'react';
|
|
3
|
+
import { Box, Typography, Stack, Button, TextField, IconButton, CircularProgress, Alert, List, ListItem, ListItemText, ListItemSecondaryAction, Tooltip, Divider, } from '@mui/material';
|
|
4
|
+
import DeleteIcon from '@mui/icons-material/Delete';
|
|
5
|
+
import { authApi } from '../auth/authApi';
|
|
6
|
+
import { FEATURES } from '../auth/authConfig';
|
|
7
|
+
const PasskeysComponent = () => {
|
|
8
|
+
const [passkeys, setPasskeys] = useState([]);
|
|
9
|
+
const [loading, setLoading] = useState(true);
|
|
10
|
+
const [reloading, setReloading] = useState(false);
|
|
11
|
+
const [name, setName] = useState('');
|
|
12
|
+
const [creating, setCreating] = useState(false);
|
|
13
|
+
const [deletingIds, setDeletingIds] = useState(new Set());
|
|
14
|
+
const [message, setMessage] = useState('');
|
|
15
|
+
const [error, setError] = useState('');
|
|
16
|
+
const passkeysSupported = typeof window !== 'undefined' && !!window.PublicKeyCredential;
|
|
17
|
+
const loadPasskeys = async () => {
|
|
18
|
+
var _a, _b;
|
|
19
|
+
setError('');
|
|
20
|
+
try {
|
|
21
|
+
const data = await authApi.fetchPasskeys();
|
|
22
|
+
setPasskeys(data);
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
const msg = ((_b = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.detail) ||
|
|
26
|
+
(err === null || err === void 0 ? void 0 : err.message) ||
|
|
27
|
+
'Could not load passkeys.';
|
|
28
|
+
setError(msg);
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
setLoading(false);
|
|
32
|
+
setReloading(false);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!FEATURES.passkeysEnabled || !passkeysSupported) {
|
|
37
|
+
setLoading(false);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
loadPasskeys();
|
|
41
|
+
}, [passkeysSupported]);
|
|
42
|
+
const handleCreate = async () => {
|
|
43
|
+
var _a, _b;
|
|
44
|
+
setMessage('');
|
|
45
|
+
setError('');
|
|
46
|
+
if (!FEATURES.passkeysEnabled || !passkeysSupported) {
|
|
47
|
+
setError('Passkeys are not available in this environment.');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
setCreating(true);
|
|
51
|
+
try {
|
|
52
|
+
const fallbackName = (name === null || name === void 0 ? void 0 : name.trim()) ||
|
|
53
|
+
`Passkey on ${navigator.platform || 'this device'}`;
|
|
54
|
+
await authApi.registerPasskey(fallbackName);
|
|
55
|
+
setMessage('Passkey added successfully.');
|
|
56
|
+
setName('');
|
|
57
|
+
setReloading(true);
|
|
58
|
+
await loadPasskeys();
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
const msg = ((_b = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.detail) ||
|
|
62
|
+
(err === null || err === void 0 ? void 0 : err.message) ||
|
|
63
|
+
'Could not add passkey.';
|
|
64
|
+
setError(msg);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
setCreating(false);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const handleDelete = async (id) => {
|
|
71
|
+
var _a, _b;
|
|
72
|
+
setMessage('');
|
|
73
|
+
setError('');
|
|
74
|
+
setDeletingIds((prev) => new Set(prev).add(id));
|
|
75
|
+
try {
|
|
76
|
+
await authApi.deletePasskey(id);
|
|
77
|
+
setPasskeys((prev) => prev.filter((pk) => pk.id !== id));
|
|
78
|
+
setMessage('Passkey deleted.');
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
const msg = ((_b = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.detail) ||
|
|
82
|
+
(err === null || err === void 0 ? void 0 : err.message) ||
|
|
83
|
+
'Could not delete passkey.';
|
|
84
|
+
setError(msg);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
setDeletingIds((prev) => {
|
|
88
|
+
const next = new Set(prev);
|
|
89
|
+
next.delete(id);
|
|
90
|
+
return next;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
if (!FEATURES.passkeysEnabled) {
|
|
95
|
+
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: "Passkeys" }), _jsx(Typography, { variant: "body2", color: "text.secondary", children: "Passkeys are disabled for this project." })] }));
|
|
96
|
+
}
|
|
97
|
+
return (_jsxs(Box, { children: [_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(Alert, { severity: "info", sx: { mb: 2 }, children: "Passkeys are not supported in this browser." })), message && (_jsx(Alert, { severity: "success", sx: { mb: 2 }, children: message })), error && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error })), passkeysSupported && (_jsx(Box, { sx: { mb: 3 }, children: _jsxs(Stack, { spacing: 2, direction: { xs: 'column', sm: 'row' }, alignItems: { xs: 'stretch', sm: 'flex-start' }, children: [_jsx(TextField, { label: "Passkey name", placeholder: "e.g. MacBook, iPhone", value: name, onChange: (e) => setName(e.target.value), disabled: creating, fullWidth: true }), _jsx(Button, { variant: "outlined", onClick: handleCreate, disabled: creating, children: creating ? 'Adding…' : 'Add passkey' })] }) })), _jsx(Divider, { sx: { mb: 2 } }), loading ? (_jsx(Box, { sx: { py: 2, display: 'flex', justifyContent: 'center' }, children: _jsx(CircularProgress, { size: 24 }) })) : (_jsxs(Box, { children: [_jsx(Typography, { variant: "subtitle1", gutterBottom: true, children: "Existing passkeys" }), reloading && (_jsx(Box, { sx: { py: 1, display: 'flex', justifyContent: 'center' }, children: _jsx(CircularProgress, { size: 20 }) })), passkeys.length === 0 ? (_jsx(Typography, { variant: "body2", color: "text.secondary", children: "No passkeys registered yet." })) : (_jsx(List, { dense: true, children: passkeys.map((pk) => (_jsxs(ListItem, { divider: true, children: [_jsx(ListItemText, { primary: pk.name || 'Passkey', secondary: pk.last_used_at
|
|
98
|
+
? `Last used: ${pk.last_used_at}`
|
|
99
|
+
: pk.created_at
|
|
100
|
+
? `Created: ${pk.created_at}`
|
|
101
|
+
: undefined }), _jsx(ListItemSecondaryAction, { children: _jsx(Tooltip, { title: "Delete passkey for this device", children: _jsx("span", { children: _jsx(IconButton, { edge: "end", "aria-label": "delete", onClick: () => handleDelete(pk.id), disabled: deletingIds.has(pk.id), size: "small", children: deletingIds.has(pk.id) ? (_jsx(CircularProgress, { size: 18 })) : (_jsx(DeleteIcon, { fontSize: "small" })) }) }) }) })] }, pk.id))) }))] }))] }));
|
|
102
|
+
};
|
|
103
|
+
export default PasskeysComponent;
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// src/auth/components/SecurityComponent.jsx
|
|
2
3
|
import React, { useState } from 'react';
|
|
3
|
-
import { Box, Typography, Divider,
|
|
4
|
+
import { Box, Typography, Divider, Alert, } from '@mui/material';
|
|
4
5
|
import PasswordChangeForm from './PasswordChangeForm';
|
|
5
6
|
import SocialLoginButtons from './SocialLoginButtons';
|
|
7
|
+
import PasskeysComponent from './PasskeysComponent'; // <--- WICHTIG
|
|
6
8
|
import { authApi } from '../auth/authApi';
|
|
7
|
-
import { FEATURES } from '../auth/authConfig';
|
|
8
9
|
const SecurityComponent = () => {
|
|
9
10
|
const [message, setMessage] = useState('');
|
|
10
11
|
const [error, setError] = useState('');
|
|
11
|
-
const [passkeyName, setPasskeyName] = useState('');
|
|
12
|
-
const [passkeySubmitting, setPasskeySubmitting] = useState(false);
|
|
13
12
|
const handleSocialClick = async (provider) => {
|
|
14
13
|
setMessage('');
|
|
15
14
|
setError('');
|
|
@@ -35,26 +34,6 @@ const SecurityComponent = () => {
|
|
|
35
34
|
setError(errorMsg);
|
|
36
35
|
}
|
|
37
36
|
};
|
|
38
|
-
|
|
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." }))] }));
|
|
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(PasskeysComponent, {})] }));
|
|
59
38
|
};
|
|
60
39
|
export default SecurityComponent;
|
package/package.json
CHANGED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Typography,
|
|
5
|
+
Stack,
|
|
6
|
+
Button,
|
|
7
|
+
TextField,
|
|
8
|
+
IconButton,
|
|
9
|
+
CircularProgress,
|
|
10
|
+
Alert,
|
|
11
|
+
List,
|
|
12
|
+
ListItem,
|
|
13
|
+
ListItemText,
|
|
14
|
+
ListItemSecondaryAction,
|
|
15
|
+
Tooltip,
|
|
16
|
+
Divider,
|
|
17
|
+
} from '@mui/material';
|
|
18
|
+
import DeleteIcon from '@mui/icons-material/Delete';
|
|
19
|
+
import { authApi } from '../auth/authApi';
|
|
20
|
+
import { FEATURES } from '../auth/authConfig';
|
|
21
|
+
|
|
22
|
+
const PasskeysComponent = () => {
|
|
23
|
+
const [passkeys, setPasskeys] = useState([]);
|
|
24
|
+
const [loading, setLoading] = useState(true);
|
|
25
|
+
const [reloading, setReloading] = useState(false);
|
|
26
|
+
|
|
27
|
+
const [name, setName] = useState('');
|
|
28
|
+
const [creating, setCreating] = useState(false);
|
|
29
|
+
const [deletingIds, setDeletingIds] = useState(new Set());
|
|
30
|
+
|
|
31
|
+
const [message, setMessage] = useState('');
|
|
32
|
+
const [error, setError] = useState('');
|
|
33
|
+
|
|
34
|
+
const passkeysSupported =
|
|
35
|
+
typeof window !== 'undefined' && !!window.PublicKeyCredential;
|
|
36
|
+
|
|
37
|
+
const loadPasskeys = async () => {
|
|
38
|
+
setError('');
|
|
39
|
+
try {
|
|
40
|
+
const data = await authApi.fetchPasskeys();
|
|
41
|
+
setPasskeys(data);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
const msg =
|
|
44
|
+
err?.response?.data?.detail ||
|
|
45
|
+
err?.message ||
|
|
46
|
+
'Could not load passkeys.';
|
|
47
|
+
setError(msg);
|
|
48
|
+
} finally {
|
|
49
|
+
setLoading(false);
|
|
50
|
+
setReloading(false);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!FEATURES.passkeysEnabled || !passkeysSupported) {
|
|
56
|
+
setLoading(false);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
loadPasskeys();
|
|
60
|
+
}, [passkeysSupported]);
|
|
61
|
+
|
|
62
|
+
const handleCreate = async () => {
|
|
63
|
+
setMessage('');
|
|
64
|
+
setError('');
|
|
65
|
+
|
|
66
|
+
if (!FEATURES.passkeysEnabled || !passkeysSupported) {
|
|
67
|
+
setError('Passkeys are not available in this environment.');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
setCreating(true);
|
|
72
|
+
try {
|
|
73
|
+
const fallbackName =
|
|
74
|
+
name?.trim() ||
|
|
75
|
+
`Passkey on ${navigator.platform || 'this device'}`;
|
|
76
|
+
|
|
77
|
+
await authApi.registerPasskey(fallbackName);
|
|
78
|
+
setMessage('Passkey added successfully.');
|
|
79
|
+
setName('');
|
|
80
|
+
|
|
81
|
+
setReloading(true);
|
|
82
|
+
await loadPasskeys();
|
|
83
|
+
} catch (err) {
|
|
84
|
+
const msg =
|
|
85
|
+
err?.response?.data?.detail ||
|
|
86
|
+
err?.message ||
|
|
87
|
+
'Could not add passkey.';
|
|
88
|
+
setError(msg);
|
|
89
|
+
} finally {
|
|
90
|
+
setCreating(false);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const handleDelete = async (id) => {
|
|
95
|
+
setMessage('');
|
|
96
|
+
setError('');
|
|
97
|
+
|
|
98
|
+
setDeletingIds((prev) => new Set(prev).add(id));
|
|
99
|
+
try {
|
|
100
|
+
await authApi.deletePasskey(id);
|
|
101
|
+
setPasskeys((prev) => prev.filter((pk) => pk.id !== id));
|
|
102
|
+
setMessage('Passkey deleted.');
|
|
103
|
+
} catch (err) {
|
|
104
|
+
const msg =
|
|
105
|
+
err?.response?.data?.detail ||
|
|
106
|
+
err?.message ||
|
|
107
|
+
'Could not delete passkey.';
|
|
108
|
+
setError(msg);
|
|
109
|
+
} finally {
|
|
110
|
+
setDeletingIds((prev) => {
|
|
111
|
+
const next = new Set(prev);
|
|
112
|
+
next.delete(id);
|
|
113
|
+
return next;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (!FEATURES.passkeysEnabled) {
|
|
119
|
+
return (
|
|
120
|
+
<Box>
|
|
121
|
+
<Typography variant="h6" gutterBottom>
|
|
122
|
+
Passkeys
|
|
123
|
+
</Typography>
|
|
124
|
+
<Typography variant="body2" color="text.secondary">
|
|
125
|
+
Passkeys are disabled for this project.
|
|
126
|
+
</Typography>
|
|
127
|
+
</Box>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<Box>
|
|
133
|
+
<Typography variant="h6" gutterBottom>
|
|
134
|
+
Passkeys
|
|
135
|
+
</Typography>
|
|
136
|
+
<Typography variant="body2" sx={{ mb: 1 }}>
|
|
137
|
+
Use passkeys for passwordless sign-in on this device.
|
|
138
|
+
</Typography>
|
|
139
|
+
|
|
140
|
+
{!passkeysSupported && (
|
|
141
|
+
<Alert severity="info" sx={{ mb: 2 }}>
|
|
142
|
+
Passkeys are not supported in this browser.
|
|
143
|
+
</Alert>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
{message && (
|
|
147
|
+
<Alert severity="success" sx={{ mb: 2 }}>
|
|
148
|
+
{message}
|
|
149
|
+
</Alert>
|
|
150
|
+
)}
|
|
151
|
+
{error && (
|
|
152
|
+
<Alert severity="error" sx={{ mb: 2 }}>
|
|
153
|
+
{error}
|
|
154
|
+
</Alert>
|
|
155
|
+
)}
|
|
156
|
+
|
|
157
|
+
{/* Add passkey form */}
|
|
158
|
+
{passkeysSupported && (
|
|
159
|
+
<Box sx={{ mb: 3 }}>
|
|
160
|
+
<Stack
|
|
161
|
+
spacing={2}
|
|
162
|
+
direction={{ xs: 'column', sm: 'row' }}
|
|
163
|
+
alignItems={{ xs: 'stretch', sm: 'flex-start' }}
|
|
164
|
+
>
|
|
165
|
+
<TextField
|
|
166
|
+
label="Passkey name"
|
|
167
|
+
placeholder="e.g. MacBook, iPhone"
|
|
168
|
+
value={name}
|
|
169
|
+
onChange={(e) => setName(e.target.value)}
|
|
170
|
+
disabled={creating}
|
|
171
|
+
fullWidth
|
|
172
|
+
/>
|
|
173
|
+
<Button
|
|
174
|
+
variant="outlined"
|
|
175
|
+
onClick={handleCreate}
|
|
176
|
+
disabled={creating}
|
|
177
|
+
>
|
|
178
|
+
{creating ? 'Adding…' : 'Add passkey'}
|
|
179
|
+
</Button>
|
|
180
|
+
</Stack>
|
|
181
|
+
</Box>
|
|
182
|
+
)}
|
|
183
|
+
|
|
184
|
+
<Divider sx={{ mb: 2 }} />
|
|
185
|
+
|
|
186
|
+
{/* List of existing passkeys */}
|
|
187
|
+
{loading ? (
|
|
188
|
+
<Box sx={{ py: 2, display: 'flex', justifyContent: 'center' }}>
|
|
189
|
+
<CircularProgress size={24} />
|
|
190
|
+
</Box>
|
|
191
|
+
) : (
|
|
192
|
+
<Box>
|
|
193
|
+
<Typography variant="subtitle1" gutterBottom>
|
|
194
|
+
Existing passkeys
|
|
195
|
+
</Typography>
|
|
196
|
+
|
|
197
|
+
{reloading && (
|
|
198
|
+
<Box sx={{ py: 1, display: 'flex', justifyContent: 'center' }}>
|
|
199
|
+
<CircularProgress size={20} />
|
|
200
|
+
</Box>
|
|
201
|
+
)}
|
|
202
|
+
|
|
203
|
+
{passkeys.length === 0 ? (
|
|
204
|
+
<Typography variant="body2" color="text.secondary">
|
|
205
|
+
No passkeys registered yet.
|
|
206
|
+
</Typography>
|
|
207
|
+
) : (
|
|
208
|
+
<List dense>
|
|
209
|
+
{passkeys.map((pk) => (
|
|
210
|
+
<ListItem key={pk.id} divider>
|
|
211
|
+
<ListItemText
|
|
212
|
+
primary={pk.name || 'Passkey'}
|
|
213
|
+
secondary={
|
|
214
|
+
pk.last_used_at
|
|
215
|
+
? `Last used: ${pk.last_used_at}`
|
|
216
|
+
: pk.created_at
|
|
217
|
+
? `Created: ${pk.created_at}`
|
|
218
|
+
: undefined
|
|
219
|
+
}
|
|
220
|
+
/>
|
|
221
|
+
<ListItemSecondaryAction>
|
|
222
|
+
<Tooltip title="Delete passkey for this device">
|
|
223
|
+
<span>
|
|
224
|
+
<IconButton
|
|
225
|
+
edge="end"
|
|
226
|
+
aria-label="delete"
|
|
227
|
+
onClick={() => handleDelete(pk.id)}
|
|
228
|
+
disabled={deletingIds.has(pk.id)}
|
|
229
|
+
size="small"
|
|
230
|
+
>
|
|
231
|
+
{deletingIds.has(pk.id) ? (
|
|
232
|
+
<CircularProgress size={18} />
|
|
233
|
+
) : (
|
|
234
|
+
<DeleteIcon fontSize="small" />
|
|
235
|
+
)}
|
|
236
|
+
</IconButton>
|
|
237
|
+
</span>
|
|
238
|
+
</Tooltip>
|
|
239
|
+
</ListItemSecondaryAction>
|
|
240
|
+
</ListItem>
|
|
241
|
+
))}
|
|
242
|
+
</List>
|
|
243
|
+
)}
|
|
244
|
+
</Box>
|
|
245
|
+
)}
|
|
246
|
+
</Box>
|
|
247
|
+
);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export default PasskeysComponent;
|
|
@@ -1,23 +1,19 @@
|
|
|
1
|
+
// src/auth/components/SecurityComponent.jsx
|
|
1
2
|
import React, { useState } from 'react';
|
|
2
3
|
import {
|
|
3
4
|
Box,
|
|
4
5
|
Typography,
|
|
5
6
|
Divider,
|
|
6
|
-
Button,
|
|
7
|
-
Stack,
|
|
8
7
|
Alert,
|
|
9
|
-
TextField,
|
|
10
8
|
} from '@mui/material';
|
|
11
9
|
import PasswordChangeForm from './PasswordChangeForm';
|
|
12
10
|
import SocialLoginButtons from './SocialLoginButtons';
|
|
11
|
+
import PasskeysComponent from './PasskeysComponent'; // <--- WICHTIG
|
|
13
12
|
import { authApi } from '../auth/authApi';
|
|
14
|
-
import { FEATURES } from '../auth/authConfig';
|
|
15
13
|
|
|
16
14
|
const SecurityComponent = () => {
|
|
17
15
|
const [message, setMessage] = useState('');
|
|
18
16
|
const [error, setError] = useState('');
|
|
19
|
-
const [passkeyName, setPasskeyName] = useState('');
|
|
20
|
-
const [passkeySubmitting, setPasskeySubmitting] = useState(false);
|
|
21
17
|
|
|
22
18
|
const handleSocialClick = async (provider) => {
|
|
23
19
|
setMessage('');
|
|
@@ -44,30 +40,6 @@ const SecurityComponent = () => {
|
|
|
44
40
|
}
|
|
45
41
|
};
|
|
46
42
|
|
|
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
|
-
|
|
71
43
|
return (
|
|
72
44
|
<Box>
|
|
73
45
|
{message && (
|
|
@@ -101,28 +73,7 @@ const SecurityComponent = () => {
|
|
|
101
73
|
<Divider sx={{ my: 3 }} />
|
|
102
74
|
|
|
103
75
|
{/* Passkeys Section */}
|
|
104
|
-
<
|
|
105
|
-
Passkeys
|
|
106
|
-
</Typography>
|
|
107
|
-
<Typography variant="body2" sx={{ mb: 1 }}>
|
|
108
|
-
Use passkeys for passwordless sign-in on this device.
|
|
109
|
-
</Typography>
|
|
110
|
-
|
|
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 */}
|
|
76
|
+
<PasskeysComponent />
|
|
126
77
|
</Box>
|
|
127
78
|
);
|
|
128
79
|
};
|