@micha.bigler/ui-core-micha 1.2.7 → 1.2.9

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.
@@ -340,17 +340,14 @@ export async function loginWithPasskey() {
340
340
  * Loads all authenticators and filters for WebAuthn passkeys.
341
341
  */
342
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');
343
+ const res = await axios.get('/api/users/passkeys/', { withCredentials: true });
344
+ return res.data;
348
345
  }
349
346
  /**
350
347
  * Deletes a single passkey authenticator by id.
351
348
  */
352
349
  export async function deletePasskey(id) {
353
- await axios.delete(`${HEADLESS_BASE}/account/authenticators/${id}/`, { withCredentials: true });
350
+ await axios.delete(`/api/users/passkeys/${id}/`, { withCredentials: true });
354
351
  }
355
352
  // -----------------------------
356
353
  // Aggregated API object
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "private": false,
@@ -448,25 +448,16 @@ export async function loginWithPasskey() {
448
448
  * Loads all authenticators and filters for WebAuthn passkeys.
449
449
  */
450
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');
451
+ const res = await axios.get('/api/users/passkeys/', { withCredentials: true });
452
+ return res.data;
460
453
  }
461
454
 
455
+
462
456
  /**
463
457
  * Deletes a single passkey authenticator by id.
464
458
  */
465
459
  export async function deletePasskey(id) {
466
- await axios.delete(
467
- `${HEADLESS_BASE}/account/authenticators/${id}/`,
468
- { withCredentials: true },
469
- );
460
+ await axios.delete(`/api/users/passkeys/${id}/`, { withCredentials: true });
470
461
  }
471
462
 
472
463
  // -----------------------------
@@ -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;