@micha.bigler/ui-core-micha 1.4.20 → 1.4.23
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/AuthContext.js +0 -1
- package/dist/auth/authApi.js +137 -378
- package/dist/components/MFAComponent.js +7 -7
- package/dist/components/MfaLoginComponent.js +6 -5
- package/dist/components/PasskeysComponent.js +5 -5
- package/dist/components/SecurityComponent.js +4 -3
- package/dist/components/SupportRecoveryRequestsTab.js +4 -4
- package/dist/components/UserInviteComponent.js +38 -0
- package/dist/components/UserListComponent.js +83 -0
- package/dist/pages/AccountPage.js +67 -23
- package/dist/pages/LoginPage.js +6 -5
- package/dist/pages/PasswordInvitePage.js +3 -3
- package/dist/pages/SignUpPage.js +3 -3
- package/dist/utils/authService.js +53 -0
- package/dist/utils/errors.js +33 -0
- package/dist/utils/webauthn.js +44 -0
- package/package.json +1 -1
- package/src/auth/AuthContext.jsx +0 -1
- package/src/auth/authApi.jsx +143 -478
- package/src/components/MFAComponent.jsx +7 -7
- package/src/components/MfaLoginComponent.jsx +6 -5
- package/src/components/PasskeysComponent.jsx +5 -5
- package/src/components/SecurityComponent.jsx +4 -3
- package/src/components/SupportRecoveryRequestsTab.jsx +4 -4
- package/src/components/UserInviteComponent.jsx +69 -0
- package/src/components/UserListComponent.jsx +167 -0
- package/src/pages/AccountPage.jsx +140 -47
- package/src/pages/LoginPage.jsx +6 -5
- package/src/pages/PasswordInvitePage.jsx +3 -3
- package/src/pages/SignUpPage.jsx +3 -3
- package/src/utils/authService.js +68 -0
- package/src/utils/errors.js +39 -0
- package/src/utils/webauthn.js +51 -0
package/src/pages/LoginPage.jsx
CHANGED
|
@@ -4,7 +4,8 @@ import { Helmet } from 'react-helmet';
|
|
|
4
4
|
import { Typography, Box, Alert } from '@mui/material';
|
|
5
5
|
import { NarrowPage } from '../layout/PageLayout';
|
|
6
6
|
import { AuthContext } from '../auth/AuthContext';
|
|
7
|
-
import {
|
|
7
|
+
import { loginWithRecoveryPassword, loginWithPassword } from '../auth/authApi';
|
|
8
|
+
import { loginWithPasskey, startSocialLogin } from '../utils/authService';
|
|
8
9
|
import LoginForm from '../components/LoginForm';
|
|
9
10
|
import MfaLoginComponent from '../components/MfaLoginComponent';
|
|
10
11
|
import { useTranslation } from 'react-i18next';
|
|
@@ -32,7 +33,7 @@ export function LoginPage() {
|
|
|
32
33
|
try {
|
|
33
34
|
// Recovery flow: password login via special endpoint, no MFA
|
|
34
35
|
if (recoveryToken) {
|
|
35
|
-
const result = await
|
|
36
|
+
const result = await loginWithRecoveryPassword(
|
|
36
37
|
identifier,
|
|
37
38
|
password,
|
|
38
39
|
recoveryToken,
|
|
@@ -45,7 +46,7 @@ export function LoginPage() {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
// Normal login flow via headless allauth
|
|
48
|
-
const result = await
|
|
49
|
+
const result = await loginWithPassword(identifier, password);
|
|
49
50
|
|
|
50
51
|
if (result.needsMfa) {
|
|
51
52
|
setMfaState({
|
|
@@ -77,7 +78,7 @@ export function LoginPage() {
|
|
|
77
78
|
setErrorKey(null);
|
|
78
79
|
setSubmitting(true);
|
|
79
80
|
try {
|
|
80
|
-
const { user } = await
|
|
81
|
+
const { user } = await loginWithPasskey();
|
|
81
82
|
login(user);
|
|
82
83
|
|
|
83
84
|
const requiresExtra =
|
|
@@ -95,7 +96,7 @@ export function LoginPage() {
|
|
|
95
96
|
}
|
|
96
97
|
};
|
|
97
98
|
|
|
98
|
-
const handleSocialLogin = (provider) =>
|
|
99
|
+
const handleSocialLogin = (provider) => startSocialLogin(provider);
|
|
99
100
|
const handleSignUp = () => navigate('/signup');
|
|
100
101
|
const handleForgotPassword = () => navigate('/reset-request-password');
|
|
101
102
|
|
|
@@ -6,7 +6,7 @@ import { Typography } from '@mui/material';
|
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
7
|
import { NarrowPage } from '../layout/PageLayout';
|
|
8
8
|
import PasswordSetForm from '../components/PasswordSetForm';
|
|
9
|
-
import {
|
|
9
|
+
import { verifyResetToken, setNewPassword } from '../auth/authApi';
|
|
10
10
|
|
|
11
11
|
export function PasswordInvitePage() {
|
|
12
12
|
const { uid, token } = useParams();
|
|
@@ -40,7 +40,7 @@ export function PasswordInvitePage() {
|
|
|
40
40
|
|
|
41
41
|
const check = async () => {
|
|
42
42
|
try {
|
|
43
|
-
await
|
|
43
|
+
await verifyResetToken(uid, token);
|
|
44
44
|
setChecked(true);
|
|
45
45
|
} catch (err) {
|
|
46
46
|
setErrorKey(err.code || 'Auth.RESET_LINK_INVALID');
|
|
@@ -62,7 +62,7 @@ export function PasswordInvitePage() {
|
|
|
62
62
|
setSuccessKey(null);
|
|
63
63
|
|
|
64
64
|
try {
|
|
65
|
-
await
|
|
65
|
+
await setNewPassword(uid, token, newPassword);
|
|
66
66
|
setSuccessKey(
|
|
67
67
|
isInvite
|
|
68
68
|
? 'Auth.RESET_PASSWORD_SUCCESS_INVITE'
|
package/src/pages/SignUpPage.jsx
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import { Helmet } from 'react-helmet';
|
|
12
12
|
import { useTranslation } from 'react-i18next';
|
|
13
13
|
import { NarrowPage } from '../layout/PageLayout';
|
|
14
|
-
import {
|
|
14
|
+
import { validateAccessCode, requestInviteWithCode } from '../auth/authApi';
|
|
15
15
|
|
|
16
16
|
export function SignUpPage() {
|
|
17
17
|
const navigate = useNavigate();
|
|
@@ -41,14 +41,14 @@ export function SignUpPage() {
|
|
|
41
41
|
|
|
42
42
|
try {
|
|
43
43
|
// 1) Access-Code prüfen
|
|
44
|
-
const res = await
|
|
44
|
+
const res = await validateAccessCode(accessCode);
|
|
45
45
|
if (!res?.valid) {
|
|
46
46
|
setErrorKey('Auth.ACCESS_CODE_INVALID_OR_INACTIVE');
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
// 2) Invite anfordern
|
|
51
|
-
await
|
|
51
|
+
await requestInviteWithCode(email, accessCode);
|
|
52
52
|
|
|
53
53
|
setSuccessKey('Auth.INVITE_REQUEST_SUCCESS');
|
|
54
54
|
} catch (err) {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// src/utils/authService.js
|
|
2
|
+
import { getPasskeyRegistrationOptions, completePasskeyRegistration, getPasskeyLoginOptions, completePasskeyLogin, fetchCurrentUser } from '../auth/authApi';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ensureWebAuthnSupport,
|
|
6
|
+
serializeCredential
|
|
7
|
+
} from '../utils/webauthn';
|
|
8
|
+
import { normaliseApiError } from '../utils/auth-errors';
|
|
9
|
+
|
|
10
|
+
export async function registerPasskey(name = 'Passkey') {
|
|
11
|
+
ensureWebAuthnSupport();
|
|
12
|
+
|
|
13
|
+
// 1. Get Options from Server
|
|
14
|
+
const creationOptions = await getPasskeyRegistrationOptions();
|
|
15
|
+
|
|
16
|
+
// 2. Call Browser API
|
|
17
|
+
let credential;
|
|
18
|
+
try {
|
|
19
|
+
// Note: Parse JSON to Options needs a helper if creationOptions is raw JSON strings
|
|
20
|
+
// modern browsers/allauth usually provide correct types, but check your library version
|
|
21
|
+
// For standard Allauth headless, you might need window.PublicKeyCredential.parseCreationOptionsFromJSON
|
|
22
|
+
const pubKey = window.PublicKeyCredential.parseCreationOptionsFromJSON(creationOptions);
|
|
23
|
+
credential = await navigator.credentials.create({ publicKey: pubKey });
|
|
24
|
+
} catch (err) {
|
|
25
|
+
if (err.name === 'NotAllowedError') {
|
|
26
|
+
throw normaliseApiError(new Error('Auth.PASSKEY_CANCELLED'), 'Auth.PASSKEY_CANCELLED');
|
|
27
|
+
}
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 3. Send back to Server
|
|
32
|
+
const credentialJson = serializeCredential(credential);
|
|
33
|
+
return completePasskeyRegistration(credentialJson, name);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function loginWithPasskey() {
|
|
37
|
+
ensureWebAuthnSupport();
|
|
38
|
+
|
|
39
|
+
// 1. Get Challenge
|
|
40
|
+
const requestOptions = await getPasskeyLoginOptions();
|
|
41
|
+
|
|
42
|
+
// 2. Browser Sign
|
|
43
|
+
let assertion;
|
|
44
|
+
try {
|
|
45
|
+
const pubKey = window.PublicKeyCredential.parseRequestOptionsFromJSON(requestOptions);
|
|
46
|
+
assertion = await navigator.credentials.get({ publicKey: pubKey });
|
|
47
|
+
} catch (err) {
|
|
48
|
+
throw normaliseApiError(new Error('Auth.PASSKEY_CANCELLED'), 'Auth.PASSKEY_CANCELLED');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 3. Complete
|
|
52
|
+
const credentialJson = serializeCredential(assertion);
|
|
53
|
+
await completePasskeyLogin(credentialJson);
|
|
54
|
+
|
|
55
|
+
// 4. Reload User
|
|
56
|
+
return fetchCurrentUser();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function startSocialLogin(provider) {
|
|
60
|
+
if (typeof window === 'undefined') {
|
|
61
|
+
throw normaliseApiError(
|
|
62
|
+
new Error('Auth.SOCIAL_LOGIN_NOT_IN_BROWSER'),
|
|
63
|
+
'Auth.SOCIAL_LOGIN_NOT_IN_BROWSER'
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
// Browser-Redirect ist ein Side-Effect -> Service Layer
|
|
67
|
+
window.location.href = `/accounts/${provider}/login/?process=login`;
|
|
68
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/utils/errors.js
|
|
2
|
+
export function extractErrorInfo(error) {
|
|
3
|
+
const status = error.response?.status ?? null;
|
|
4
|
+
const data = error.response?.data ?? null;
|
|
5
|
+
|
|
6
|
+
if (!data) {
|
|
7
|
+
return { status, code: null, message: error.message || null, raw: null };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Allauth Headless structure often nests errors in "errors" array or "status" key
|
|
11
|
+
if (Array.isArray(data.errors) && data.errors.length > 0) {
|
|
12
|
+
// Pick the first error code
|
|
13
|
+
const first = data.errors[0];
|
|
14
|
+
return { status, code: first.code, message: first.message, raw: data };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (typeof data.code === 'string') {
|
|
18
|
+
return { status, code: data.code, message: null, raw: data };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Fallback for generic Django errors
|
|
22
|
+
if (typeof data.detail === 'string') {
|
|
23
|
+
return { status, code: 'GENERIC', message: data.detail, raw: data };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return { status, code: null, message: null, raw: data };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function normaliseApiError(error, defaultCode = 'Auth.GENERIC_ERROR') {
|
|
30
|
+
const info = extractErrorInfo(error);
|
|
31
|
+
const code = info.code || defaultCode;
|
|
32
|
+
const message = info.message || code || defaultCode;
|
|
33
|
+
|
|
34
|
+
const err = new Error(message);
|
|
35
|
+
err.code = code;
|
|
36
|
+
err.status = info.status;
|
|
37
|
+
err.raw = info.raw;
|
|
38
|
+
return err;
|
|
39
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/utils/webauthn.js
|
|
2
|
+
export function bufferToBase64URL(buffer) {
|
|
3
|
+
const bytes = new Uint8Array(buffer);
|
|
4
|
+
let str = '';
|
|
5
|
+
for (const char of bytes) {
|
|
6
|
+
str += String.fromCharCode(char);
|
|
7
|
+
}
|
|
8
|
+
const base64 = btoa(str);
|
|
9
|
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function serializeCredential(credential) {
|
|
13
|
+
const p = {
|
|
14
|
+
id: credential.id,
|
|
15
|
+
rawId: bufferToBase64URL(credential.rawId),
|
|
16
|
+
type: credential.type,
|
|
17
|
+
response: {
|
|
18
|
+
clientDataJSON: bufferToBase64URL(credential.response.clientDataJSON),
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
if (credential.response.attestationObject) {
|
|
23
|
+
p.response.attestationObject = bufferToBase64URL(credential.response.attestationObject);
|
|
24
|
+
}
|
|
25
|
+
if (credential.response.authenticatorData) {
|
|
26
|
+
p.response.authenticatorData = bufferToBase64URL(credential.response.authenticatorData);
|
|
27
|
+
}
|
|
28
|
+
if (credential.response.signature) {
|
|
29
|
+
p.response.signature = bufferToBase64URL(credential.response.signature);
|
|
30
|
+
}
|
|
31
|
+
if (credential.response.userHandle) {
|
|
32
|
+
p.response.userHandle = bufferToBase64URL(credential.response.userHandle);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typeof credential.getClientExtensionResults === 'function') {
|
|
36
|
+
p.clientExtensionResults = credential.getClientExtensionResults();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return p;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function ensureWebAuthnSupport() {
|
|
43
|
+
if (
|
|
44
|
+
typeof window === 'undefined' ||
|
|
45
|
+
typeof navigator === 'undefined' ||
|
|
46
|
+
!window.PublicKeyCredential ||
|
|
47
|
+
!navigator.credentials
|
|
48
|
+
) {
|
|
49
|
+
throw new Error('Auth.PASSKEY_NOT_SUPPORTED');
|
|
50
|
+
}
|
|
51
|
+
}
|