@micha.bigler/ui-core-micha 1.2.1 → 1.2.3
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
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
2
|
import { HEADLESS_BASE, USERS_BASE, ACCESS_CODES_BASE } from './authConfig';
|
|
3
|
-
//
|
|
3
|
+
// -----------------------------
|
|
4
|
+
// Error helper
|
|
5
|
+
// -----------------------------
|
|
4
6
|
function extractErrorMessage(error) {
|
|
5
7
|
var _a;
|
|
6
8
|
const data = (_a = error.response) === null || _a === void 0 ? void 0 : _a.data;
|
|
@@ -15,65 +17,64 @@ function extractErrorMessage(error) {
|
|
|
15
17
|
}
|
|
16
18
|
return 'An error occurred. Please try again.';
|
|
17
19
|
}
|
|
18
|
-
//
|
|
20
|
+
// -----------------------------
|
|
21
|
+
// CSRF helper
|
|
22
|
+
// -----------------------------
|
|
19
23
|
function getCsrfToken() {
|
|
20
|
-
if (!document.cookie)
|
|
24
|
+
if (typeof document === 'undefined' || !document.cookie) {
|
|
21
25
|
return null;
|
|
26
|
+
}
|
|
22
27
|
const match = document.cookie.match(/csrftoken=([^;]+)/);
|
|
23
28
|
return match ? match[1] : null;
|
|
24
29
|
}
|
|
30
|
+
// -----------------------------
|
|
31
|
+
// WebAuthn capability helpers
|
|
32
|
+
// -----------------------------
|
|
25
33
|
const hasJsonWebAuthn = typeof window !== 'undefined' &&
|
|
26
|
-
window.PublicKeyCredential &&
|
|
34
|
+
typeof window.PublicKeyCredential !== 'undefined' &&
|
|
27
35
|
typeof window.PublicKeyCredential.parseCreationOptionsFromJSON === 'function' &&
|
|
28
36
|
typeof window.PublicKeyCredential.parseRequestOptionsFromJSON === 'function';
|
|
29
37
|
function ensureWebAuthnSupport() {
|
|
30
|
-
if (
|
|
38
|
+
if (typeof window === 'undefined' ||
|
|
39
|
+
typeof navigator === 'undefined' ||
|
|
40
|
+
!window.PublicKeyCredential ||
|
|
41
|
+
!navigator.credentials) {
|
|
31
42
|
throw new Error('Passkeys are not supported in this browser.');
|
|
32
43
|
}
|
|
33
44
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
// -----------------------------
|
|
46
|
+
// User-related helpers
|
|
47
|
+
// -----------------------------
|
|
37
48
|
export async function fetchCurrentUser() {
|
|
38
49
|
const res = await axios.get(`${USERS_BASE}/current/`, {
|
|
39
50
|
withCredentials: true,
|
|
40
51
|
});
|
|
41
52
|
return res.data;
|
|
42
53
|
}
|
|
43
|
-
/**
|
|
44
|
-
* Updates the user profile fields.
|
|
45
|
-
*/
|
|
46
54
|
export async function updateUserProfile(data) {
|
|
47
55
|
const res = await axios.patch(`${USERS_BASE}/current/`, data, {
|
|
48
56
|
withCredentials: true,
|
|
49
57
|
});
|
|
50
58
|
return res.data;
|
|
51
59
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
// -----------------------------
|
|
61
|
+
// Authentication: password
|
|
62
|
+
// -----------------------------
|
|
55
63
|
export async function loginWithPassword(email, password) {
|
|
56
64
|
try {
|
|
57
|
-
await axios.post(`${HEADLESS_BASE}/auth/login`, {
|
|
58
|
-
email,
|
|
59
|
-
password,
|
|
60
|
-
}, { withCredentials: true });
|
|
65
|
+
await axios.post(`${HEADLESS_BASE}/auth/login`, { email, password }, { withCredentials: true });
|
|
61
66
|
}
|
|
62
67
|
catch (error) {
|
|
63
68
|
if (error.response && error.response.status === 409) {
|
|
64
|
-
//
|
|
69
|
+
// Already logged in: continue and fetch current user
|
|
65
70
|
}
|
|
66
71
|
else {
|
|
67
72
|
throw new Error(extractErrorMessage(error));
|
|
68
73
|
}
|
|
69
74
|
}
|
|
70
75
|
const user = await fetchCurrentUser();
|
|
71
|
-
// Normalise return shape so callers always get { user }
|
|
72
76
|
return { user };
|
|
73
77
|
}
|
|
74
|
-
/**
|
|
75
|
-
* Requests a password reset email via allauth headless.
|
|
76
|
-
*/
|
|
77
78
|
export async function requestPasswordReset(email) {
|
|
78
79
|
try {
|
|
79
80
|
await axios.post(`${USERS_BASE}/reset-request/`, { email }, { withCredentials: true });
|
|
@@ -82,23 +83,15 @@ export async function requestPasswordReset(email) {
|
|
|
82
83
|
throw new Error(extractErrorMessage(error));
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
|
-
|
|
86
|
-
* Sets a new password using a reset key (from email link).
|
|
87
|
-
*/
|
|
86
|
+
// (Falls du es noch brauchst: klassischer allauth-Key-Flow)
|
|
88
87
|
export async function resetPasswordWithKey(key, newPassword) {
|
|
89
88
|
try {
|
|
90
|
-
await axios.post(`${HEADLESS_BASE}/auth/password/reset/key`, {
|
|
91
|
-
key,
|
|
92
|
-
password: newPassword,
|
|
93
|
-
}, { withCredentials: true });
|
|
89
|
+
await axios.post(`${HEADLESS_BASE}/auth/password/reset/key`, { key, password: newPassword }, { withCredentials: true });
|
|
94
90
|
}
|
|
95
91
|
catch (error) {
|
|
96
92
|
throw new Error(extractErrorMessage(error));
|
|
97
93
|
}
|
|
98
94
|
}
|
|
99
|
-
/**
|
|
100
|
-
* Changes the password for an authenticated user.
|
|
101
|
-
*/
|
|
102
95
|
export async function changePassword(currentPassword, newPassword) {
|
|
103
96
|
try {
|
|
104
97
|
await axios.post(`${HEADLESS_BASE}/account/password/change`, {
|
|
@@ -110,9 +103,9 @@ export async function changePassword(currentPassword, newPassword) {
|
|
|
110
103
|
throw new Error(extractErrorMessage(error));
|
|
111
104
|
}
|
|
112
105
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
106
|
+
// -----------------------------
|
|
107
|
+
// Logout / Session
|
|
108
|
+
// -----------------------------
|
|
116
109
|
export async function logoutSession() {
|
|
117
110
|
try {
|
|
118
111
|
const headers = {};
|
|
@@ -127,35 +120,36 @@ export async function logoutSession() {
|
|
|
127
120
|
}
|
|
128
121
|
catch (error) {
|
|
129
122
|
if (error.response && [401, 404, 410].includes(error.response.status)) {
|
|
123
|
+
// Session already gone, nothing to do
|
|
130
124
|
return;
|
|
131
125
|
}
|
|
132
126
|
// eslint-disable-next-line no-console
|
|
133
127
|
console.error('Logout error:', error);
|
|
134
128
|
}
|
|
135
129
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
130
|
+
export async function fetchHeadlessSession() {
|
|
131
|
+
const res = await axios.get(`${HEADLESS_BASE}/auth/session`, {
|
|
132
|
+
withCredentials: true,
|
|
133
|
+
});
|
|
134
|
+
return res.data;
|
|
135
|
+
}
|
|
136
|
+
// -----------------------------
|
|
137
|
+
// Social login
|
|
138
|
+
// -----------------------------
|
|
143
139
|
export function startSocialLogin(provider) {
|
|
140
|
+
if (typeof window === 'undefined') {
|
|
141
|
+
throw new Error('Social login is only available in a browser environment.');
|
|
142
|
+
}
|
|
143
|
+
// Classic allauth HTML flow
|
|
144
144
|
window.location.href = `/accounts/${provider}/login/?process=login`;
|
|
145
145
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
* Antwort: { valid: true/false } oder 400 mit detail-Fehler.
|
|
150
|
-
*/
|
|
146
|
+
// -----------------------------
|
|
147
|
+
// Invitation / access code
|
|
148
|
+
// -----------------------------
|
|
151
149
|
export async function validateAccessCode(code) {
|
|
152
150
|
const res = await axios.post(`${ACCESS_CODES_BASE}/validate/`, { code }, { withCredentials: true });
|
|
153
|
-
return res.data;
|
|
151
|
+
return res.data;
|
|
154
152
|
}
|
|
155
|
-
/**
|
|
156
|
-
* Fordert eine Einladung mit optionalem Access-Code an.
|
|
157
|
-
* Backend prüft den Code noch einmal serverseitig.
|
|
158
|
-
*/
|
|
159
153
|
export async function requestInviteWithCode(email, accessCode) {
|
|
160
154
|
const payload = { email };
|
|
161
155
|
if (accessCode) {
|
|
@@ -164,15 +158,9 @@ export async function requestInviteWithCode(email, accessCode) {
|
|
|
164
158
|
const res = await axios.post(`${USERS_BASE}/invite/`, payload, { withCredentials: true });
|
|
165
159
|
return res.data;
|
|
166
160
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
export async function fetchHeadlessSession() {
|
|
171
|
-
const res = await axios.get(`${HEADLESS_BASE}/auth/session`, {
|
|
172
|
-
withCredentials: true,
|
|
173
|
-
});
|
|
174
|
-
return res.data;
|
|
175
|
-
}
|
|
161
|
+
// -----------------------------
|
|
162
|
+
// Custom password-reset via uid/token
|
|
163
|
+
// -----------------------------
|
|
176
164
|
export async function verifyResetToken(uid, token) {
|
|
177
165
|
const res = await axios.get(`${USERS_BASE}/password-reset/${uid}/${token}/`, { withCredentials: true });
|
|
178
166
|
return res.data;
|
|
@@ -181,16 +169,22 @@ export async function setNewPassword(uid, token, newPassword) {
|
|
|
181
169
|
const res = await axios.post(`${USERS_BASE}/password-reset/${uid}/${token}/`, { new_password: newPassword }, { withCredentials: true });
|
|
182
170
|
return res.data;
|
|
183
171
|
}
|
|
184
|
-
//
|
|
172
|
+
// -----------------------------
|
|
173
|
+
// WebAuthn / Passkeys: register
|
|
174
|
+
// -----------------------------
|
|
185
175
|
async function registerPasskeyStart({ passwordless = true } = {}) {
|
|
186
176
|
ensureWebAuthnSupport();
|
|
187
177
|
const res = await axios.get(`${HEADLESS_BASE}/account/authenticators/webauthn`, {
|
|
188
178
|
params: passwordless ? { passwordless: true } : {},
|
|
189
179
|
withCredentials: true,
|
|
190
180
|
});
|
|
191
|
-
|
|
181
|
+
const data = res.data || {};
|
|
182
|
+
// allauth.headless: { creation_options: { publicKey: { ... } } }
|
|
183
|
+
const creationOptionsJson = (data.creation_options && data.creation_options.publicKey) ||
|
|
184
|
+
data.creation_options ||
|
|
185
|
+
data;
|
|
186
|
+
return creationOptionsJson;
|
|
192
187
|
}
|
|
193
|
-
// Complete: Credential an Server schicken
|
|
194
188
|
async function registerPasskeyComplete(credentialJson, name = 'Passkey') {
|
|
195
189
|
const res = await axios.post(`${HEADLESS_BASE}/account/authenticators/webauthn`, {
|
|
196
190
|
credential: credentialJson,
|
|
@@ -198,53 +192,93 @@ async function registerPasskeyComplete(credentialJson, name = 'Passkey') {
|
|
|
198
192
|
}, { withCredentials: true });
|
|
199
193
|
return res.data;
|
|
200
194
|
}
|
|
201
|
-
// High-level Helper, den das UI aufruft
|
|
202
195
|
export async function registerPasskey(name = 'Passkey') {
|
|
203
196
|
ensureWebAuthnSupport();
|
|
204
|
-
const optionsJson = await registerPasskeyStart({ passwordless: true });
|
|
205
197
|
if (!hasJsonWebAuthn) {
|
|
206
|
-
throw new Error('Passkey JSON helpers not available in this browser.');
|
|
198
|
+
throw new Error('Passkey JSON helpers are not available in this browser.');
|
|
199
|
+
}
|
|
200
|
+
const creationOptionsJson = await registerPasskeyStart({ passwordless: true });
|
|
201
|
+
let credential;
|
|
202
|
+
try {
|
|
203
|
+
const publicKeyOptions = window.PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJson);
|
|
204
|
+
credential = await navigator.credentials.create({
|
|
205
|
+
publicKey: publicKeyOptions,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
if (err && err.name === 'NotAllowedError') {
|
|
210
|
+
throw new Error('Passkey creation was cancelled by the user.');
|
|
211
|
+
}
|
|
212
|
+
throw err;
|
|
207
213
|
}
|
|
208
|
-
const publicKeyOptions = window.PublicKeyCredential.parseCreationOptionsFromJSON(optionsJson);
|
|
209
|
-
const credential = await navigator.credentials.create({
|
|
210
|
-
publicKey: publicKeyOptions,
|
|
211
|
-
});
|
|
212
214
|
if (!credential) {
|
|
213
215
|
throw new Error('Passkey creation was cancelled.');
|
|
214
216
|
}
|
|
215
217
|
const credentialJson = credential.toJSON();
|
|
216
218
|
return registerPasskeyComplete(credentialJson, name);
|
|
217
219
|
}
|
|
220
|
+
// -----------------------------
|
|
221
|
+
// WebAuthn / Passkeys: login
|
|
222
|
+
// -----------------------------
|
|
218
223
|
async function loginWithPasskeyStart() {
|
|
219
224
|
ensureWebAuthnSupport();
|
|
220
225
|
const res = await axios.get(`${HEADLESS_BASE}/auth/webauthn/login`, { withCredentials: true });
|
|
221
|
-
|
|
226
|
+
const data = res.data || {};
|
|
227
|
+
// allauth.headless: { request_options: { publicKey: { ... } } }
|
|
228
|
+
const requestOptionsJson = (data.request_options && data.request_options.publicKey) ||
|
|
229
|
+
data.request_options ||
|
|
230
|
+
data;
|
|
231
|
+
return requestOptionsJson;
|
|
222
232
|
}
|
|
223
233
|
async function loginWithPasskeyComplete(credentialJson) {
|
|
224
234
|
const res = await axios.post(`${HEADLESS_BASE}/auth/webauthn/login`, { credential: credentialJson }, { withCredentials: true });
|
|
225
235
|
return res.data;
|
|
226
236
|
}
|
|
227
|
-
// Public API, die du im UI verwendest
|
|
228
237
|
export async function loginWithPasskey() {
|
|
229
238
|
ensureWebAuthnSupport();
|
|
230
|
-
const optionsJson = await loginWithPasskeyStart();
|
|
231
239
|
if (!hasJsonWebAuthn) {
|
|
232
|
-
throw new Error('Passkey JSON helpers not available in this browser.');
|
|
240
|
+
throw new Error('Passkey JSON helpers are not available in this browser.');
|
|
241
|
+
}
|
|
242
|
+
const requestOptionsJson = await loginWithPasskeyStart();
|
|
243
|
+
let assertion;
|
|
244
|
+
try {
|
|
245
|
+
const publicKeyOptions = window.PublicKeyCredential.parseRequestOptionsFromJSON(requestOptionsJson);
|
|
246
|
+
assertion = await navigator.credentials.get({
|
|
247
|
+
publicKey: publicKeyOptions,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
if (err && err.name === 'NotAllowedError') {
|
|
252
|
+
throw new Error('Passkey authentication was cancelled by the user.');
|
|
253
|
+
}
|
|
254
|
+
throw err;
|
|
233
255
|
}
|
|
234
|
-
const publicKeyOptions = window.PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson);
|
|
235
|
-
const assertion = await navigator.credentials.get({
|
|
236
|
-
publicKey: publicKeyOptions,
|
|
237
|
-
});
|
|
238
256
|
if (!assertion) {
|
|
239
257
|
throw new Error('Passkey authentication was cancelled.');
|
|
240
258
|
}
|
|
241
259
|
const credentialJson = assertion.toJSON();
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
260
|
+
let postError = null;
|
|
261
|
+
try {
|
|
262
|
+
await loginWithPasskeyComplete(credentialJson);
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
// For example 401 although credential looked fine
|
|
266
|
+
postError = err;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const user = await fetchCurrentUser();
|
|
270
|
+
return { user };
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
if (postError) {
|
|
274
|
+
throw new Error(extractErrorMessage(postError));
|
|
275
|
+
}
|
|
276
|
+
throw new Error(extractErrorMessage(err));
|
|
277
|
+
}
|
|
247
278
|
}
|
|
279
|
+
// -----------------------------
|
|
280
|
+
// Aggregated API object
|
|
281
|
+
// -----------------------------
|
|
248
282
|
export const authApi = {
|
|
249
283
|
fetchCurrentUser,
|
|
250
284
|
updateUserProfile,
|
|
@@ -261,3 +295,4 @@ export const authApi = {
|
|
|
261
295
|
validateAccessCode,
|
|
262
296
|
requestInviteWithCode,
|
|
263
297
|
};
|
|
298
|
+
export default authApi;
|
|
@@ -4,15 +4,16 @@ import { Box, Typography, Divider, Button, Stack, Alert, } from '@mui/material';
|
|
|
4
4
|
import PasswordChangeForm from './PasswordChangeForm';
|
|
5
5
|
import SocialLoginButtons from './SocialLoginButtons';
|
|
6
6
|
import { authApi } from '../auth/authApi';
|
|
7
|
+
import { FEATURES } from '../auth/authConfig';
|
|
7
8
|
const SecurityComponent = () => {
|
|
8
9
|
const [message, setMessage] = useState('');
|
|
9
10
|
const [error, setError] = useState('');
|
|
10
|
-
const handleSocialClick =
|
|
11
|
+
const handleSocialClick = (provider) => {
|
|
11
12
|
setMessage('');
|
|
12
13
|
setError('');
|
|
13
14
|
try {
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
authApi.startSocialLogin(provider);
|
|
16
|
+
// Ab hier verlässt der Browser normalerweise die Seite
|
|
16
17
|
}
|
|
17
18
|
catch (e) {
|
|
18
19
|
setError(e.message || 'Social login could not be started.');
|
package/dist/pages/LoginPage.js
CHANGED
package/package.json
CHANGED
package/src/auth/authApi.jsx
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
-
import { HEADLESS_BASE, USERS_BASE, ACCESS_CODES_BASE } from './authConfig';
|
|
2
|
+
import { HEADLESS_BASE, USERS_BASE, ACCESS_CODES_BASE } from './authConfig';
|
|
3
3
|
|
|
4
|
-
//
|
|
4
|
+
// -----------------------------
|
|
5
|
+
// Error helper
|
|
6
|
+
// -----------------------------
|
|
5
7
|
function extractErrorMessage(error) {
|
|
6
8
|
const data = error.response?.data;
|
|
7
9
|
if (!data) {
|
|
@@ -16,28 +18,40 @@ function extractErrorMessage(error) {
|
|
|
16
18
|
return 'An error occurred. Please try again.';
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
//
|
|
21
|
+
// -----------------------------
|
|
22
|
+
// CSRF helper
|
|
23
|
+
// -----------------------------
|
|
20
24
|
function getCsrfToken() {
|
|
21
|
-
if (!document.cookie)
|
|
25
|
+
if (typeof document === 'undefined' || !document.cookie) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
22
28
|
const match = document.cookie.match(/csrftoken=([^;]+)/);
|
|
23
29
|
return match ? match[1] : null;
|
|
24
30
|
}
|
|
25
31
|
|
|
32
|
+
// -----------------------------
|
|
33
|
+
// WebAuthn capability helpers
|
|
34
|
+
// -----------------------------
|
|
26
35
|
const hasJsonWebAuthn =
|
|
27
36
|
typeof window !== 'undefined' &&
|
|
28
|
-
window.PublicKeyCredential &&
|
|
37
|
+
typeof window.PublicKeyCredential !== 'undefined' &&
|
|
29
38
|
typeof window.PublicKeyCredential.parseCreationOptionsFromJSON === 'function' &&
|
|
30
39
|
typeof window.PublicKeyCredential.parseRequestOptionsFromJSON === 'function';
|
|
31
40
|
|
|
32
41
|
function ensureWebAuthnSupport() {
|
|
33
|
-
if (
|
|
42
|
+
if (
|
|
43
|
+
typeof window === 'undefined' ||
|
|
44
|
+
typeof navigator === 'undefined' ||
|
|
45
|
+
!window.PublicKeyCredential ||
|
|
46
|
+
!navigator.credentials
|
|
47
|
+
) {
|
|
34
48
|
throw new Error('Passkeys are not supported in this browser.');
|
|
35
49
|
}
|
|
36
50
|
}
|
|
37
51
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
52
|
+
// -----------------------------
|
|
53
|
+
// User-related helpers
|
|
54
|
+
// -----------------------------
|
|
41
55
|
export async function fetchCurrentUser() {
|
|
42
56
|
const res = await axios.get(`${USERS_BASE}/current/`, {
|
|
43
57
|
withCredentials: true,
|
|
@@ -45,9 +59,6 @@ export async function fetchCurrentUser() {
|
|
|
45
59
|
return res.data;
|
|
46
60
|
}
|
|
47
61
|
|
|
48
|
-
/**
|
|
49
|
-
* Updates the user profile fields.
|
|
50
|
-
*/
|
|
51
62
|
export async function updateUserProfile(data) {
|
|
52
63
|
const res = await axios.patch(`${USERS_BASE}/current/`, data, {
|
|
53
64
|
withCredentials: true,
|
|
@@ -55,35 +66,28 @@ export async function updateUserProfile(data) {
|
|
|
55
66
|
return res.data;
|
|
56
67
|
}
|
|
57
68
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
// -----------------------------
|
|
70
|
+
// Authentication: password
|
|
71
|
+
// -----------------------------
|
|
61
72
|
export async function loginWithPassword(email, password) {
|
|
62
73
|
try {
|
|
63
74
|
await axios.post(
|
|
64
75
|
`${HEADLESS_BASE}/auth/login`,
|
|
65
|
-
{
|
|
66
|
-
email,
|
|
67
|
-
password,
|
|
68
|
-
},
|
|
76
|
+
{ email, password },
|
|
69
77
|
{ withCredentials: true },
|
|
70
78
|
);
|
|
71
79
|
} catch (error) {
|
|
72
80
|
if (error.response && error.response.status === 409) {
|
|
73
|
-
//
|
|
81
|
+
// Already logged in: continue and fetch current user
|
|
74
82
|
} else {
|
|
75
83
|
throw new Error(extractErrorMessage(error));
|
|
76
84
|
}
|
|
77
85
|
}
|
|
78
86
|
|
|
79
87
|
const user = await fetchCurrentUser();
|
|
80
|
-
// Normalise return shape so callers always get { user }
|
|
81
88
|
return { user };
|
|
82
89
|
}
|
|
83
90
|
|
|
84
|
-
/**
|
|
85
|
-
* Requests a password reset email via allauth headless.
|
|
86
|
-
*/
|
|
87
91
|
export async function requestPasswordReset(email) {
|
|
88
92
|
try {
|
|
89
93
|
await axios.post(
|
|
@@ -96,17 +100,12 @@ export async function requestPasswordReset(email) {
|
|
|
96
100
|
}
|
|
97
101
|
}
|
|
98
102
|
|
|
99
|
-
|
|
100
|
-
* Sets a new password using a reset key (from email link).
|
|
101
|
-
*/
|
|
103
|
+
// (Falls du es noch brauchst: klassischer allauth-Key-Flow)
|
|
102
104
|
export async function resetPasswordWithKey(key, newPassword) {
|
|
103
105
|
try {
|
|
104
106
|
await axios.post(
|
|
105
107
|
`${HEADLESS_BASE}/auth/password/reset/key`,
|
|
106
|
-
{
|
|
107
|
-
key,
|
|
108
|
-
password: newPassword,
|
|
109
|
-
},
|
|
108
|
+
{ key, password: newPassword },
|
|
110
109
|
{ withCredentials: true },
|
|
111
110
|
);
|
|
112
111
|
} catch (error) {
|
|
@@ -114,9 +113,6 @@ export async function resetPasswordWithKey(key, newPassword) {
|
|
|
114
113
|
}
|
|
115
114
|
}
|
|
116
115
|
|
|
117
|
-
/**
|
|
118
|
-
* Changes the password for an authenticated user.
|
|
119
|
-
*/
|
|
120
116
|
export async function changePassword(currentPassword, newPassword) {
|
|
121
117
|
try {
|
|
122
118
|
await axios.post(
|
|
@@ -132,9 +128,9 @@ export async function changePassword(currentPassword, newPassword) {
|
|
|
132
128
|
}
|
|
133
129
|
}
|
|
134
130
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
131
|
+
// -----------------------------
|
|
132
|
+
// Logout / Session
|
|
133
|
+
// -----------------------------
|
|
138
134
|
export async function logoutSession() {
|
|
139
135
|
try {
|
|
140
136
|
const headers = {};
|
|
@@ -145,13 +141,14 @@ export async function logoutSession() {
|
|
|
145
141
|
|
|
146
142
|
await axios.delete(
|
|
147
143
|
`${HEADLESS_BASE}/auth/session`,
|
|
148
|
-
{
|
|
144
|
+
{
|
|
149
145
|
withCredentials: true,
|
|
150
|
-
headers,
|
|
146
|
+
headers,
|
|
151
147
|
},
|
|
152
148
|
);
|
|
153
149
|
} catch (error) {
|
|
154
150
|
if (error.response && [401, 404, 410].includes(error.response.status)) {
|
|
151
|
+
// Session already gone, nothing to do
|
|
155
152
|
return;
|
|
156
153
|
}
|
|
157
154
|
// eslint-disable-next-line no-console
|
|
@@ -159,35 +156,37 @@ export async function logoutSession() {
|
|
|
159
156
|
}
|
|
160
157
|
}
|
|
161
158
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
159
|
+
export async function fetchHeadlessSession() {
|
|
160
|
+
const res = await axios.get(`${HEADLESS_BASE}/auth/session`, {
|
|
161
|
+
withCredentials: true,
|
|
162
|
+
});
|
|
163
|
+
return res.data;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// -----------------------------
|
|
167
|
+
// Social login
|
|
168
|
+
// -----------------------------
|
|
169
169
|
export function startSocialLogin(provider) {
|
|
170
|
+
if (typeof window === 'undefined') {
|
|
171
|
+
throw new Error('Social login is only available in a browser environment.');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Classic allauth HTML flow
|
|
170
175
|
window.location.href = `/accounts/${provider}/login/?process=login`;
|
|
171
176
|
}
|
|
172
177
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
* Antwort: { valid: true/false } oder 400 mit detail-Fehler.
|
|
177
|
-
*/
|
|
178
|
+
// -----------------------------
|
|
179
|
+
// Invitation / access code
|
|
180
|
+
// -----------------------------
|
|
178
181
|
export async function validateAccessCode(code) {
|
|
179
182
|
const res = await axios.post(
|
|
180
183
|
`${ACCESS_CODES_BASE}/validate/`,
|
|
181
184
|
{ code },
|
|
182
185
|
{ withCredentials: true },
|
|
183
186
|
);
|
|
184
|
-
return res.data;
|
|
187
|
+
return res.data;
|
|
185
188
|
}
|
|
186
189
|
|
|
187
|
-
/**
|
|
188
|
-
* Fordert eine Einladung mit optionalem Access-Code an.
|
|
189
|
-
* Backend prüft den Code noch einmal serverseitig.
|
|
190
|
-
*/
|
|
191
190
|
export async function requestInviteWithCode(email, accessCode) {
|
|
192
191
|
const payload = { email };
|
|
193
192
|
if (accessCode) {
|
|
@@ -202,19 +201,9 @@ export async function requestInviteWithCode(email, accessCode) {
|
|
|
202
201
|
return res.data;
|
|
203
202
|
}
|
|
204
203
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
*/
|
|
209
|
-
export async function fetchHeadlessSession() {
|
|
210
|
-
const res = await axios.get(`${HEADLESS_BASE}/auth/session`, {
|
|
211
|
-
withCredentials: true,
|
|
212
|
-
});
|
|
213
|
-
return res.data;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
204
|
+
// -----------------------------
|
|
205
|
+
// Custom password-reset via uid/token
|
|
206
|
+
// -----------------------------
|
|
218
207
|
export async function verifyResetToken(uid, token) {
|
|
219
208
|
const res = await axios.get(
|
|
220
209
|
`${USERS_BASE}/password-reset/${uid}/${token}/`,
|
|
@@ -232,7 +221,9 @@ export async function setNewPassword(uid, token, newPassword) {
|
|
|
232
221
|
return res.data;
|
|
233
222
|
}
|
|
234
223
|
|
|
235
|
-
//
|
|
224
|
+
// -----------------------------
|
|
225
|
+
// WebAuthn / Passkeys: register
|
|
226
|
+
// -----------------------------
|
|
236
227
|
async function registerPasskeyStart({ passwordless = true } = {}) {
|
|
237
228
|
ensureWebAuthnSupport();
|
|
238
229
|
|
|
@@ -243,10 +234,18 @@ async function registerPasskeyStart({ passwordless = true } = {}) {
|
|
|
243
234
|
withCredentials: true,
|
|
244
235
|
},
|
|
245
236
|
);
|
|
246
|
-
|
|
237
|
+
|
|
238
|
+
const data = res.data || {};
|
|
239
|
+
|
|
240
|
+
// allauth.headless: { creation_options: { publicKey: { ... } } }
|
|
241
|
+
const creationOptionsJson =
|
|
242
|
+
(data.creation_options && data.creation_options.publicKey) ||
|
|
243
|
+
data.creation_options ||
|
|
244
|
+
data;
|
|
245
|
+
|
|
246
|
+
return creationOptionsJson;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
// Complete: Credential an Server schicken
|
|
250
249
|
async function registerPasskeyComplete(credentialJson, name = 'Passkey') {
|
|
251
250
|
const res = await axios.post(
|
|
252
251
|
`${HEADLESS_BASE}/account/authenticators/webauthn`,
|
|
@@ -259,22 +258,29 @@ async function registerPasskeyComplete(credentialJson, name = 'Passkey') {
|
|
|
259
258
|
return res.data;
|
|
260
259
|
}
|
|
261
260
|
|
|
262
|
-
// High-level Helper, den das UI aufruft
|
|
263
261
|
export async function registerPasskey(name = 'Passkey') {
|
|
264
262
|
ensureWebAuthnSupport();
|
|
265
263
|
|
|
266
|
-
const optionsJson = await registerPasskeyStart({ passwordless: true });
|
|
267
|
-
|
|
268
264
|
if (!hasJsonWebAuthn) {
|
|
269
|
-
throw new Error('Passkey JSON helpers not available in this browser.');
|
|
265
|
+
throw new Error('Passkey JSON helpers are not available in this browser.');
|
|
270
266
|
}
|
|
271
267
|
|
|
272
|
-
const
|
|
273
|
-
window.PublicKeyCredential.parseCreationOptionsFromJSON(optionsJson);
|
|
268
|
+
const creationOptionsJson = await registerPasskeyStart({ passwordless: true });
|
|
274
269
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
270
|
+
let credential;
|
|
271
|
+
try {
|
|
272
|
+
const publicKeyOptions =
|
|
273
|
+
window.PublicKeyCredential.parseCreationOptionsFromJSON(creationOptionsJson);
|
|
274
|
+
|
|
275
|
+
credential = await navigator.credentials.create({
|
|
276
|
+
publicKey: publicKeyOptions,
|
|
277
|
+
});
|
|
278
|
+
} catch (err) {
|
|
279
|
+
if (err && err.name === 'NotAllowedError') {
|
|
280
|
+
throw new Error('Passkey creation was cancelled by the user.');
|
|
281
|
+
}
|
|
282
|
+
throw err;
|
|
283
|
+
}
|
|
278
284
|
|
|
279
285
|
if (!credential) {
|
|
280
286
|
throw new Error('Passkey creation was cancelled.');
|
|
@@ -284,6 +290,9 @@ export async function registerPasskey(name = 'Passkey') {
|
|
|
284
290
|
return registerPasskeyComplete(credentialJson, name);
|
|
285
291
|
}
|
|
286
292
|
|
|
293
|
+
// -----------------------------
|
|
294
|
+
// WebAuthn / Passkeys: login
|
|
295
|
+
// -----------------------------
|
|
287
296
|
async function loginWithPasskeyStart() {
|
|
288
297
|
ensureWebAuthnSupport();
|
|
289
298
|
|
|
@@ -291,7 +300,16 @@ async function loginWithPasskeyStart() {
|
|
|
291
300
|
`${HEADLESS_BASE}/auth/webauthn/login`,
|
|
292
301
|
{ withCredentials: true },
|
|
293
302
|
);
|
|
294
|
-
|
|
303
|
+
|
|
304
|
+
const data = res.data || {};
|
|
305
|
+
|
|
306
|
+
// allauth.headless: { request_options: { publicKey: { ... } } }
|
|
307
|
+
const requestOptionsJson =
|
|
308
|
+
(data.request_options && data.request_options.publicKey) ||
|
|
309
|
+
data.request_options ||
|
|
310
|
+
data;
|
|
311
|
+
|
|
312
|
+
return requestOptionsJson;
|
|
295
313
|
}
|
|
296
314
|
|
|
297
315
|
async function loginWithPasskeyComplete(credentialJson) {
|
|
@@ -303,22 +321,29 @@ async function loginWithPasskeyComplete(credentialJson) {
|
|
|
303
321
|
return res.data;
|
|
304
322
|
}
|
|
305
323
|
|
|
306
|
-
// Public API, die du im UI verwendest
|
|
307
324
|
export async function loginWithPasskey() {
|
|
308
325
|
ensureWebAuthnSupport();
|
|
309
326
|
|
|
310
|
-
const optionsJson = await loginWithPasskeyStart();
|
|
311
|
-
|
|
312
327
|
if (!hasJsonWebAuthn) {
|
|
313
|
-
throw new Error('Passkey JSON helpers not available in this browser.');
|
|
328
|
+
throw new Error('Passkey JSON helpers are not available in this browser.');
|
|
314
329
|
}
|
|
315
330
|
|
|
316
|
-
const
|
|
317
|
-
window.PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson);
|
|
331
|
+
const requestOptionsJson = await loginWithPasskeyStart();
|
|
318
332
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
333
|
+
let assertion;
|
|
334
|
+
try {
|
|
335
|
+
const publicKeyOptions =
|
|
336
|
+
window.PublicKeyCredential.parseRequestOptionsFromJSON(requestOptionsJson);
|
|
337
|
+
|
|
338
|
+
assertion = await navigator.credentials.get({
|
|
339
|
+
publicKey: publicKeyOptions,
|
|
340
|
+
});
|
|
341
|
+
} catch (err) {
|
|
342
|
+
if (err && err.name === 'NotAllowedError') {
|
|
343
|
+
throw new Error('Passkey authentication was cancelled by the user.');
|
|
344
|
+
}
|
|
345
|
+
throw err;
|
|
346
|
+
}
|
|
322
347
|
|
|
323
348
|
if (!assertion) {
|
|
324
349
|
throw new Error('Passkey authentication was cancelled.');
|
|
@@ -326,15 +351,28 @@ export async function loginWithPasskey() {
|
|
|
326
351
|
|
|
327
352
|
const credentialJson = assertion.toJSON();
|
|
328
353
|
|
|
329
|
-
|
|
330
|
-
|
|
354
|
+
let postError = null;
|
|
355
|
+
try {
|
|
356
|
+
await loginWithPasskeyComplete(credentialJson);
|
|
357
|
+
} catch (err) {
|
|
358
|
+
// For example 401 although credential looked fine
|
|
359
|
+
postError = err;
|
|
360
|
+
}
|
|
331
361
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
362
|
+
try {
|
|
363
|
+
const user = await fetchCurrentUser();
|
|
364
|
+
return { user };
|
|
365
|
+
} catch (err) {
|
|
366
|
+
if (postError) {
|
|
367
|
+
throw new Error(extractErrorMessage(postError));
|
|
368
|
+
}
|
|
369
|
+
throw new Error(extractErrorMessage(err));
|
|
370
|
+
}
|
|
335
371
|
}
|
|
336
372
|
|
|
337
|
-
|
|
373
|
+
// -----------------------------
|
|
374
|
+
// Aggregated API object
|
|
375
|
+
// -----------------------------
|
|
338
376
|
export const authApi = {
|
|
339
377
|
fetchCurrentUser,
|
|
340
378
|
updateUserProfile,
|
|
@@ -350,4 +388,6 @@ export const authApi = {
|
|
|
350
388
|
registerPasskey,
|
|
351
389
|
validateAccessCode,
|
|
352
390
|
requestInviteWithCode,
|
|
353
|
-
};
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
export default authApi;
|
|
@@ -10,17 +10,18 @@ import {
|
|
|
10
10
|
import PasswordChangeForm from './PasswordChangeForm';
|
|
11
11
|
import SocialLoginButtons from './SocialLoginButtons';
|
|
12
12
|
import { authApi } from '../auth/authApi';
|
|
13
|
+
import { FEATURES } from '../auth/authConfig';
|
|
13
14
|
|
|
14
15
|
const SecurityComponent = () => {
|
|
15
16
|
const [message, setMessage] = useState('');
|
|
16
17
|
const [error, setError] = useState('');
|
|
17
18
|
|
|
18
|
-
const handleSocialClick =
|
|
19
|
+
const handleSocialClick = (provider) => {
|
|
19
20
|
setMessage('');
|
|
20
21
|
setError('');
|
|
21
22
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
authApi.startSocialLogin(provider);
|
|
24
|
+
// Ab hier verlässt der Browser normalerweise die Seite
|
|
24
25
|
} catch (e) {
|
|
25
26
|
setError(e.message || 'Social login could not be started.');
|
|
26
27
|
}
|
package/src/pages/LoginPage.jsx
CHANGED