@micha.bigler/ui-core-micha 1.1.0 → 1.2.0
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 +77 -9
- package/dist/auth/authConfig.js +1 -1
- package/dist/auth/webauthnClient.js +9 -0
- package/dist/components/AccessCodeManager.js +3 -6
- package/dist/components/LoginForm.js +1 -1
- package/dist/components/PasswordSetForm.js +1 -1
- package/dist/components/SecurityComponent.js +11 -1
- package/dist/pages/SignUpPage.js +1 -0
- package/package.json +1 -1
- package/src/auth/authApi.jsx +119 -10
- package/src/auth/authConfig.jsx +1 -1
- package/src/auth/webauthnClient.jsx +18 -0
- package/src/components/AccessCodeManager.jsx +4 -7
- package/src/components/LoginForm.jsx +12 -0
- package/src/components/PasswordSetForm.jsx +1 -1
- package/src/components/SecurityComponent.jsx +15 -5
- package/src/pages/SignUpPage.jsx +1 -0
package/dist/auth/authApi.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
-
import { HEADLESS_BASE, USERS_BASE } from './authConfig';
|
|
2
|
+
import { HEADLESS_BASE, USERS_BASE, ACCESS_CODES_BASE } from './authConfig';
|
|
3
3
|
// Helper to normalise error messages from API responses
|
|
4
4
|
function extractErrorMessage(error) {
|
|
5
5
|
var _a;
|
|
@@ -22,6 +22,15 @@ function getCsrfToken() {
|
|
|
22
22
|
const match = document.cookie.match(/csrftoken=([^;]+)/);
|
|
23
23
|
return match ? match[1] : null;
|
|
24
24
|
}
|
|
25
|
+
const hasJsonWebAuthn = typeof window !== 'undefined' &&
|
|
26
|
+
window.PublicKeyCredential &&
|
|
27
|
+
typeof window.PublicKeyCredential.parseCreationOptionsFromJSON === 'function' &&
|
|
28
|
+
typeof window.PublicKeyCredential.parseRequestOptionsFromJSON === 'function';
|
|
29
|
+
function ensureWebAuthnSupport() {
|
|
30
|
+
if (!window.PublicKeyCredential || !navigator.credentials) {
|
|
31
|
+
throw new Error('Passkeys are not supported in this browser.');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
25
34
|
/**
|
|
26
35
|
* Fetches the current authenticated user from your own User API.
|
|
27
36
|
*/
|
|
@@ -52,14 +61,15 @@ export async function loginWithPassword(email, password) {
|
|
|
52
61
|
}
|
|
53
62
|
catch (error) {
|
|
54
63
|
if (error.response && error.response.status === 409) {
|
|
55
|
-
//
|
|
64
|
+
// User is already logged in, continue and fetch current user
|
|
56
65
|
}
|
|
57
66
|
else {
|
|
58
67
|
throw new Error(extractErrorMessage(error));
|
|
59
68
|
}
|
|
60
69
|
}
|
|
61
70
|
const user = await fetchCurrentUser();
|
|
62
|
-
return user
|
|
71
|
+
// Normalise return shape so callers always get { user }
|
|
72
|
+
return { user };
|
|
63
73
|
}
|
|
64
74
|
/**
|
|
65
75
|
* Requests a password reset email via allauth headless.
|
|
@@ -163,12 +173,6 @@ export async function fetchHeadlessSession() {
|
|
|
163
173
|
});
|
|
164
174
|
return res.data;
|
|
165
175
|
}
|
|
166
|
-
export async function loginWithPasskey() {
|
|
167
|
-
throw new Error('Passkey login is not implemented yet.');
|
|
168
|
-
}
|
|
169
|
-
export async function registerPasskey() {
|
|
170
|
-
throw new Error('Passkey registration is not implemented yet.');
|
|
171
|
-
}
|
|
172
176
|
export async function verifyResetToken(uid, token) {
|
|
173
177
|
const res = await axios.get(`${USERS_BASE}/password-reset/${uid}/${token}/`, { withCredentials: true });
|
|
174
178
|
return res.data;
|
|
@@ -177,6 +181,70 @@ export async function setNewPassword(uid, token, newPassword) {
|
|
|
177
181
|
const res = await axios.post(`${USERS_BASE}/password-reset/${uid}/${token}/`, { new_password: newPassword }, { withCredentials: true });
|
|
178
182
|
return res.data;
|
|
179
183
|
}
|
|
184
|
+
// Start: Optionen für Credential-Erzeugung holen
|
|
185
|
+
async function registerPasskeyStart({ passwordless = true } = {}) {
|
|
186
|
+
ensureWebAuthnSupport();
|
|
187
|
+
const res = await axios.get(`${HEADLESS_BASE}/account/authenticators/webauthn`, {
|
|
188
|
+
params: passwordless ? { passwordless: true } : {},
|
|
189
|
+
withCredentials: true,
|
|
190
|
+
});
|
|
191
|
+
return res.data; // JSON-Options vom Server
|
|
192
|
+
}
|
|
193
|
+
// Complete: Credential an Server schicken
|
|
194
|
+
async function registerPasskeyComplete(credentialJson, name = 'Passkey') {
|
|
195
|
+
const res = await axios.post(`${HEADLESS_BASE}/account/authenticators/webauthn`, {
|
|
196
|
+
credential: credentialJson,
|
|
197
|
+
name,
|
|
198
|
+
}, { withCredentials: true });
|
|
199
|
+
return res.data;
|
|
200
|
+
}
|
|
201
|
+
// High-level Helper, den das UI aufruft
|
|
202
|
+
export async function registerPasskey(name = 'Passkey') {
|
|
203
|
+
ensureWebAuthnSupport();
|
|
204
|
+
const optionsJson = await registerPasskeyStart({ passwordless: true });
|
|
205
|
+
if (!hasJsonWebAuthn) {
|
|
206
|
+
throw new Error('Passkey JSON helpers not available in this browser.');
|
|
207
|
+
}
|
|
208
|
+
const publicKeyOptions = window.PublicKeyCredential.parseCreationOptionsFromJSON(optionsJson);
|
|
209
|
+
const credential = await navigator.credentials.create({
|
|
210
|
+
publicKey: publicKeyOptions,
|
|
211
|
+
});
|
|
212
|
+
if (!credential) {
|
|
213
|
+
throw new Error('Passkey creation was cancelled.');
|
|
214
|
+
}
|
|
215
|
+
const credentialJson = credential.toJSON();
|
|
216
|
+
return registerPasskeyComplete(credentialJson, name);
|
|
217
|
+
}
|
|
218
|
+
async function loginWithPasskeyStart() {
|
|
219
|
+
ensureWebAuthnSupport();
|
|
220
|
+
const res = await axios.get(`${HEADLESS_BASE}/auth/webauthn/login`, { withCredentials: true });
|
|
221
|
+
return res.data;
|
|
222
|
+
}
|
|
223
|
+
async function loginWithPasskeyComplete(credentialJson) {
|
|
224
|
+
const res = await axios.post(`${HEADLESS_BASE}/auth/webauthn/login`, { credential: credentialJson }, { withCredentials: true });
|
|
225
|
+
return res.data;
|
|
226
|
+
}
|
|
227
|
+
// Public API, die du im UI verwendest
|
|
228
|
+
export async function loginWithPasskey() {
|
|
229
|
+
ensureWebAuthnSupport();
|
|
230
|
+
const optionsJson = await loginWithPasskeyStart();
|
|
231
|
+
if (!hasJsonWebAuthn) {
|
|
232
|
+
throw new Error('Passkey JSON helpers not available in this browser.');
|
|
233
|
+
}
|
|
234
|
+
const publicKeyOptions = window.PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson);
|
|
235
|
+
const assertion = await navigator.credentials.get({
|
|
236
|
+
publicKey: publicKeyOptions,
|
|
237
|
+
});
|
|
238
|
+
if (!assertion) {
|
|
239
|
+
throw new Error('Passkey authentication was cancelled.');
|
|
240
|
+
}
|
|
241
|
+
const credentialJson = assertion.toJSON();
|
|
242
|
+
// Allauth kann hier 200 (eingeloggt) oder 401 (z.B. Email noch nicht verifiziert) liefern :contentReference[oaicite:5]{index=5}
|
|
243
|
+
await loginWithPasskeyComplete(credentialJson);
|
|
244
|
+
// Danach wie bei Passwort-Login aktuellen User laden
|
|
245
|
+
const user = await fetchCurrentUser();
|
|
246
|
+
return user;
|
|
247
|
+
}
|
|
180
248
|
export const authApi = {
|
|
181
249
|
fetchCurrentUser,
|
|
182
250
|
updateUserProfile,
|
package/dist/auth/authConfig.js
CHANGED
|
@@ -20,6 +20,6 @@ export const SOCIAL_PROVIDERS = {
|
|
|
20
20
|
};
|
|
21
21
|
// Feature-Flags (falls du später Dinge toggeln willst)
|
|
22
22
|
export const FEATURES = {
|
|
23
|
-
passkeysEnabled:
|
|
23
|
+
passkeysEnabled: true, // kannst du später auf true setzen
|
|
24
24
|
mfaEnabled: true,
|
|
25
25
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export async function registerPasskeyStart() {
|
|
2
|
+
const res = await axios.post(`${HEADLESS_BASE}/mfa/webauthn/register/start`, {}, { withCredentials: true });
|
|
3
|
+
return res.data; // enthält challenge, rpId etc.
|
|
4
|
+
}
|
|
5
|
+
export async function registerPasskeyComplete(publicKeyCredential) {
|
|
6
|
+
const payload = serializePublicKeyCredential(publicKeyCredential);
|
|
7
|
+
const res = await axios.post(`${HEADLESS_BASE}/mfa/webauthn/register/complete`, payload, { withCredentials: true });
|
|
8
|
+
return res.data;
|
|
9
|
+
}
|
|
@@ -50,12 +50,9 @@ export function AccessCodeManager() {
|
|
|
50
50
|
// Generates a random code with the selected length
|
|
51
51
|
const generateRandomCode = (len) => {
|
|
52
52
|
const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
out += alphabet[idx];
|
|
57
|
-
}
|
|
58
|
-
return out;
|
|
53
|
+
const array = new Uint32Array(len);
|
|
54
|
+
window.crypto.getRandomValues(array);
|
|
55
|
+
return Array.from(array, (x) => alphabet[x % alphabet.length]).join('');
|
|
59
56
|
};
|
|
60
57
|
const handleCreateCode = async (code) => {
|
|
61
58
|
var _a, _b;
|
|
@@ -19,6 +19,6 @@ error, disabled = false, }) => {
|
|
|
19
19
|
alignItems: 'center',
|
|
20
20
|
mt: 1,
|
|
21
21
|
gap: 1,
|
|
22
|
-
}, children: [_jsxs(Box, { sx: { display: 'flex', gap: 1 }, children: [_jsx(Button, { type: "submit", variant: "contained", disabled: disabled, children: "Login" }), onSignUp && (_jsx(Button, { type: "button", variant: "outlined", onClick: onSignUp, disabled: disabled, children: "Sign up" }))] }), _jsx(Button, { type: "button", variant: "outlined", onClick: onForgotPassword, disabled: disabled, children: "Forgot password?" })] }), _jsx(Divider, { sx: { my: 2 }, children: "or" }), _jsx(SocialLoginButtons, { onProviderClick: onSocialLogin })] }));
|
|
22
|
+
}, children: [_jsxs(Box, { sx: { display: 'flex', gap: 1 }, children: [_jsx(Button, { type: "submit", variant: "contained", disabled: disabled, children: "Login" }), onSignUp && (_jsx(Button, { type: "button", variant: "outlined", onClick: onSignUp, disabled: disabled, children: "Sign up" }))] }), _jsx(Button, { type: "button", variant: "outlined", onClick: onForgotPassword, disabled: disabled, children: "Forgot password?" })] }), _jsx(Divider, { sx: { my: 2 }, children: "or" }), _jsx(SocialLoginButtons, { onProviderClick: onSocialLogin }), _jsx(Divider, { sx: { my: 2 }, children: "or" }), _jsx(Box, { sx: { mt: 2, textAlign: 'center' }, children: _jsx(Button, { variant: "text", onClick: handlePasskeyLogin, disabled: submitting || !window.PublicKeyCredential, children: "Use passkey" }) })] }));
|
|
23
23
|
};
|
|
24
24
|
export default LoginForm;
|
|
@@ -25,6 +25,6 @@ const PasswordSetForm = ({ onSubmit, submitting = false }) => {
|
|
|
25
25
|
onSubmit(password1);
|
|
26
26
|
}
|
|
27
27
|
};
|
|
28
|
-
return (_jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { display: 'flex', flexDirection: 'column', gap: 2, mt: 2 }, children: [localError && (_jsx(Box, { sx: {
|
|
28
|
+
return (_jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { display: 'flex', flexDirection: 'column', gap: 2, mt: 2 }, children: [localError && (_jsx(Box, { sx: { color: 'error.main', fontSize: 14 }, children: localError })), _jsx(TextField, { label: "New password", type: "password", fullWidth: true, autoComplete: "new-password", value: password1, onChange: (e) => setPassword1(e.target.value), disabled: submitting }), _jsx(TextField, { label: "Confirm new password", type: "password", fullWidth: true, autoComplete: "new-password", value: password2, onChange: (e) => setPassword2(e.target.value), disabled: submitting }), _jsx(Button, { type: "submit", variant: "contained", disabled: submitting, children: "Set password" })] }));
|
|
29
29
|
};
|
|
30
30
|
export default PasswordSetForm;
|
|
@@ -33,6 +33,16 @@ const SecurityComponent = () => {
|
|
|
33
33
|
setError(errorMsg);
|
|
34
34
|
}
|
|
35
35
|
};
|
|
36
|
-
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
|
|
36
|
+
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 () => {
|
|
37
|
+
setMessage('');
|
|
38
|
+
setError('');
|
|
39
|
+
try {
|
|
40
|
+
await authApi.registerPasskey('Default passkey');
|
|
41
|
+
setMessage('Passkey added successfully.');
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
setError(e.message || 'Could not add passkey.');
|
|
45
|
+
}
|
|
46
|
+
}, children: FEATURES.passkeysEnabled ? 'Add passkey' : 'Passkeys disabled' }) })] }));
|
|
37
47
|
};
|
|
38
48
|
export default SecurityComponent;
|
package/dist/pages/SignUpPage.js
CHANGED
|
@@ -6,6 +6,7 @@ import { Box, TextField, Button, Typography, Alert, } from '@mui/material';
|
|
|
6
6
|
import { Helmet } from 'react-helmet';
|
|
7
7
|
import { NarrowPage } from '../layout/PageLayout';
|
|
8
8
|
import { authApi } from '../auth/authApi';
|
|
9
|
+
import { ACCESS_CODES_BASE } from '../auth/authConfig';
|
|
9
10
|
export function SignUpPage() {
|
|
10
11
|
const navigate = useNavigate();
|
|
11
12
|
const [email, setEmail] = useState('');
|
package/package.json
CHANGED
package/src/auth/authApi.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
-
import { HEADLESS_BASE, USERS_BASE } from './authConfig';
|
|
2
|
+
import { HEADLESS_BASE, USERS_BASE, ACCESS_CODES_BASE } from './authConfig';
|
|
3
3
|
|
|
4
4
|
// Helper to normalise error messages from API responses
|
|
5
5
|
function extractErrorMessage(error) {
|
|
@@ -23,6 +23,18 @@ function getCsrfToken() {
|
|
|
23
23
|
return match ? match[1] : null;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
const hasJsonWebAuthn =
|
|
27
|
+
typeof window !== 'undefined' &&
|
|
28
|
+
window.PublicKeyCredential &&
|
|
29
|
+
typeof window.PublicKeyCredential.parseCreationOptionsFromJSON === 'function' &&
|
|
30
|
+
typeof window.PublicKeyCredential.parseRequestOptionsFromJSON === 'function';
|
|
31
|
+
|
|
32
|
+
function ensureWebAuthnSupport() {
|
|
33
|
+
if (!window.PublicKeyCredential || !navigator.credentials) {
|
|
34
|
+
throw new Error('Passkeys are not supported in this browser.');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
26
38
|
/**
|
|
27
39
|
* Fetches the current authenticated user from your own User API.
|
|
28
40
|
*/
|
|
@@ -51,21 +63,22 @@ export async function loginWithPassword(email, password) {
|
|
|
51
63
|
await axios.post(
|
|
52
64
|
`${HEADLESS_BASE}/auth/login`,
|
|
53
65
|
{
|
|
54
|
-
email,
|
|
66
|
+
email,
|
|
55
67
|
password,
|
|
56
68
|
},
|
|
57
69
|
{ withCredentials: true },
|
|
58
70
|
);
|
|
59
71
|
} catch (error) {
|
|
60
72
|
if (error.response && error.response.status === 409) {
|
|
61
|
-
|
|
73
|
+
// User is already logged in, continue and fetch current user
|
|
62
74
|
} else {
|
|
63
75
|
throw new Error(extractErrorMessage(error));
|
|
64
76
|
}
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
const user = await fetchCurrentUser();
|
|
68
|
-
return user
|
|
80
|
+
// Normalise return shape so callers always get { user }
|
|
81
|
+
return { user };
|
|
69
82
|
}
|
|
70
83
|
|
|
71
84
|
/**
|
|
@@ -200,13 +213,7 @@ export async function fetchHeadlessSession() {
|
|
|
200
213
|
return res.data;
|
|
201
214
|
}
|
|
202
215
|
|
|
203
|
-
export async function loginWithPasskey() {
|
|
204
|
-
throw new Error('Passkey login is not implemented yet.');
|
|
205
|
-
}
|
|
206
216
|
|
|
207
|
-
export async function registerPasskey() {
|
|
208
|
-
throw new Error('Passkey registration is not implemented yet.');
|
|
209
|
-
}
|
|
210
217
|
|
|
211
218
|
export async function verifyResetToken(uid, token) {
|
|
212
219
|
const res = await axios.get(
|
|
@@ -225,6 +232,108 @@ export async function setNewPassword(uid, token, newPassword) {
|
|
|
225
232
|
return res.data;
|
|
226
233
|
}
|
|
227
234
|
|
|
235
|
+
// Start: Optionen für Credential-Erzeugung holen
|
|
236
|
+
async function registerPasskeyStart({ passwordless = true } = {}) {
|
|
237
|
+
ensureWebAuthnSupport();
|
|
238
|
+
|
|
239
|
+
const res = await axios.get(
|
|
240
|
+
`${HEADLESS_BASE}/account/authenticators/webauthn`,
|
|
241
|
+
{
|
|
242
|
+
params: passwordless ? { passwordless: true } : {},
|
|
243
|
+
withCredentials: true,
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
return res.data; // JSON-Options vom Server
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Complete: Credential an Server schicken
|
|
250
|
+
async function registerPasskeyComplete(credentialJson, name = 'Passkey') {
|
|
251
|
+
const res = await axios.post(
|
|
252
|
+
`${HEADLESS_BASE}/account/authenticators/webauthn`,
|
|
253
|
+
{
|
|
254
|
+
credential: credentialJson,
|
|
255
|
+
name,
|
|
256
|
+
},
|
|
257
|
+
{ withCredentials: true },
|
|
258
|
+
);
|
|
259
|
+
return res.data;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// High-level Helper, den das UI aufruft
|
|
263
|
+
export async function registerPasskey(name = 'Passkey') {
|
|
264
|
+
ensureWebAuthnSupport();
|
|
265
|
+
|
|
266
|
+
const optionsJson = await registerPasskeyStart({ passwordless: true });
|
|
267
|
+
|
|
268
|
+
if (!hasJsonWebAuthn) {
|
|
269
|
+
throw new Error('Passkey JSON helpers not available in this browser.');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const publicKeyOptions =
|
|
273
|
+
window.PublicKeyCredential.parseCreationOptionsFromJSON(optionsJson);
|
|
274
|
+
|
|
275
|
+
const credential = await navigator.credentials.create({
|
|
276
|
+
publicKey: publicKeyOptions,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
if (!credential) {
|
|
280
|
+
throw new Error('Passkey creation was cancelled.');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const credentialJson = credential.toJSON();
|
|
284
|
+
return registerPasskeyComplete(credentialJson, name);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function loginWithPasskeyStart() {
|
|
288
|
+
ensureWebAuthnSupport();
|
|
289
|
+
|
|
290
|
+
const res = await axios.get(
|
|
291
|
+
`${HEADLESS_BASE}/auth/webauthn/login`,
|
|
292
|
+
{ withCredentials: true },
|
|
293
|
+
);
|
|
294
|
+
return res.data;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function loginWithPasskeyComplete(credentialJson) {
|
|
298
|
+
const res = await axios.post(
|
|
299
|
+
`${HEADLESS_BASE}/auth/webauthn/login`,
|
|
300
|
+
{ credential: credentialJson },
|
|
301
|
+
{ withCredentials: true },
|
|
302
|
+
);
|
|
303
|
+
return res.data;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Public API, die du im UI verwendest
|
|
307
|
+
export async function loginWithPasskey() {
|
|
308
|
+
ensureWebAuthnSupport();
|
|
309
|
+
|
|
310
|
+
const optionsJson = await loginWithPasskeyStart();
|
|
311
|
+
|
|
312
|
+
if (!hasJsonWebAuthn) {
|
|
313
|
+
throw new Error('Passkey JSON helpers not available in this browser.');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const publicKeyOptions =
|
|
317
|
+
window.PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson);
|
|
318
|
+
|
|
319
|
+
const assertion = await navigator.credentials.get({
|
|
320
|
+
publicKey: publicKeyOptions,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (!assertion) {
|
|
324
|
+
throw new Error('Passkey authentication was cancelled.');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const credentialJson = assertion.toJSON();
|
|
328
|
+
|
|
329
|
+
// Allauth kann hier 200 (eingeloggt) oder 401 (z.B. Email noch nicht verifiziert) liefern :contentReference[oaicite:5]{index=5}
|
|
330
|
+
await loginWithPasskeyComplete(credentialJson);
|
|
331
|
+
|
|
332
|
+
// Danach wie bei Passwort-Login aktuellen User laden
|
|
333
|
+
const user = await fetchCurrentUser();
|
|
334
|
+
return user;
|
|
335
|
+
}
|
|
336
|
+
|
|
228
337
|
|
|
229
338
|
export const authApi = {
|
|
230
339
|
fetchCurrentUser,
|
package/src/auth/authConfig.jsx
CHANGED
|
@@ -29,6 +29,6 @@ export const SOCIAL_PROVIDERS = {
|
|
|
29
29
|
|
|
30
30
|
// Feature-Flags (falls du später Dinge toggeln willst)
|
|
31
31
|
export const FEATURES = {
|
|
32
|
-
passkeysEnabled:
|
|
32
|
+
passkeysEnabled: true, // kannst du später auf true setzen
|
|
33
33
|
mfaEnabled: true,
|
|
34
34
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export async function registerPasskeyStart() {
|
|
2
|
+
const res = await axios.post(
|
|
3
|
+
`${HEADLESS_BASE}/mfa/webauthn/register/start`,
|
|
4
|
+
{},
|
|
5
|
+
{ withCredentials: true },
|
|
6
|
+
);
|
|
7
|
+
return res.data; // enthält challenge, rpId etc.
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function registerPasskeyComplete(publicKeyCredential) {
|
|
11
|
+
const payload = serializePublicKeyCredential(publicKeyCredential);
|
|
12
|
+
const res = await axios.post(
|
|
13
|
+
`${HEADLESS_BASE}/mfa/webauthn/register/complete`,
|
|
14
|
+
payload,
|
|
15
|
+
{ withCredentials: true },
|
|
16
|
+
);
|
|
17
|
+
return res.data;
|
|
18
|
+
}
|
|
@@ -63,14 +63,11 @@ export function AccessCodeManager() {
|
|
|
63
63
|
}, []);
|
|
64
64
|
|
|
65
65
|
// Generates a random code with the selected length
|
|
66
|
-
|
|
66
|
+
const generateRandomCode = (len) => {
|
|
67
67
|
const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
out += alphabet[idx];
|
|
72
|
-
}
|
|
73
|
-
return out;
|
|
68
|
+
const array = new Uint32Array(len);
|
|
69
|
+
window.crypto.getRandomValues(array);
|
|
70
|
+
return Array.from(array, (x) => alphabet[x % alphabet.length]).join('');
|
|
74
71
|
};
|
|
75
72
|
|
|
76
73
|
const handleCreateCode = async (code) => {
|
|
@@ -101,6 +101,18 @@ const LoginForm = ({
|
|
|
101
101
|
<Divider sx={{ my: 2 }}>or</Divider>
|
|
102
102
|
|
|
103
103
|
<SocialLoginButtons onProviderClick={onSocialLogin} />
|
|
104
|
+
|
|
105
|
+
<Divider sx={{ my: 2 }}>or</Divider>
|
|
106
|
+
|
|
107
|
+
<Box sx={{ mt: 2, textAlign: 'center' }}>
|
|
108
|
+
<Button
|
|
109
|
+
variant="text"
|
|
110
|
+
onClick={handlePasskeyLogin}
|
|
111
|
+
disabled={submitting || !window.PublicKeyCredential}
|
|
112
|
+
>
|
|
113
|
+
Use passkey
|
|
114
|
+
</Button>
|
|
115
|
+
</Box>
|
|
104
116
|
</Box>
|
|
105
117
|
);
|
|
106
118
|
};
|
|
@@ -37,7 +37,7 @@ const PasswordSetForm = ({ onSubmit, submitting = false }) => {
|
|
|
37
37
|
sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 2 }}
|
|
38
38
|
>
|
|
39
39
|
{localError && (
|
|
40
|
-
<Box sx={{
|
|
40
|
+
<Box sx={{ color: 'error.main', fontSize: 14 }}>
|
|
41
41
|
{localError}
|
|
42
42
|
</Box>
|
|
43
43
|
)}
|
|
@@ -75,18 +75,28 @@ const SecurityComponent = () => {
|
|
|
75
75
|
|
|
76
76
|
{/* Passkeys Section (Placeholder) */}
|
|
77
77
|
<Typography variant="h6" gutterBottom>
|
|
78
|
-
Passkeys
|
|
78
|
+
Passkeys
|
|
79
79
|
</Typography>
|
|
80
80
|
<Typography variant="body2" sx={{ mb: 1 }}>
|
|
81
|
-
Use passkeys for passwordless sign-in
|
|
82
|
-
WebAuthn endpoints are wired in the backend.
|
|
81
|
+
Use passkeys for passwordless sign-in on this device.
|
|
83
82
|
</Typography>
|
|
83
|
+
|
|
84
84
|
<Stack direction="row" spacing={2}>
|
|
85
85
|
<Button
|
|
86
86
|
variant="outlined"
|
|
87
|
-
disabled
|
|
87
|
+
disabled={!FEATURES.passkeysEnabled}
|
|
88
|
+
onClick={async () => {
|
|
89
|
+
setMessage('');
|
|
90
|
+
setError('');
|
|
91
|
+
try {
|
|
92
|
+
await authApi.registerPasskey('Default passkey');
|
|
93
|
+
setMessage('Passkey added successfully.');
|
|
94
|
+
} catch (e) {
|
|
95
|
+
setError(e.message || 'Could not add passkey.');
|
|
96
|
+
}
|
|
97
|
+
}}
|
|
88
98
|
>
|
|
89
|
-
Add passkey
|
|
99
|
+
{FEATURES.passkeysEnabled ? 'Add passkey' : 'Passkeys disabled'}
|
|
90
100
|
</Button>
|
|
91
101
|
</Stack>
|
|
92
102
|
</Box>
|
package/src/pages/SignUpPage.jsx
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { Helmet } from 'react-helmet';
|
|
12
12
|
import { NarrowPage } from '../layout/PageLayout';
|
|
13
13
|
import { authApi } from '../auth/authApi';
|
|
14
|
+
import { ACCESS_CODES_BASE } from '../auth/authConfig';
|
|
14
15
|
|
|
15
16
|
export function SignUpPage() {
|
|
16
17
|
const navigate = useNavigate();
|