@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.
Files changed (33) hide show
  1. package/dist/auth/AuthContext.js +0 -1
  2. package/dist/auth/authApi.js +137 -378
  3. package/dist/components/MFAComponent.js +7 -7
  4. package/dist/components/MfaLoginComponent.js +6 -5
  5. package/dist/components/PasskeysComponent.js +5 -5
  6. package/dist/components/SecurityComponent.js +4 -3
  7. package/dist/components/SupportRecoveryRequestsTab.js +4 -4
  8. package/dist/components/UserInviteComponent.js +38 -0
  9. package/dist/components/UserListComponent.js +83 -0
  10. package/dist/pages/AccountPage.js +67 -23
  11. package/dist/pages/LoginPage.js +6 -5
  12. package/dist/pages/PasswordInvitePage.js +3 -3
  13. package/dist/pages/SignUpPage.js +3 -3
  14. package/dist/utils/authService.js +53 -0
  15. package/dist/utils/errors.js +33 -0
  16. package/dist/utils/webauthn.js +44 -0
  17. package/package.json +1 -1
  18. package/src/auth/AuthContext.jsx +0 -1
  19. package/src/auth/authApi.jsx +143 -478
  20. package/src/components/MFAComponent.jsx +7 -7
  21. package/src/components/MfaLoginComponent.jsx +6 -5
  22. package/src/components/PasskeysComponent.jsx +5 -5
  23. package/src/components/SecurityComponent.jsx +4 -3
  24. package/src/components/SupportRecoveryRequestsTab.jsx +4 -4
  25. package/src/components/UserInviteComponent.jsx +69 -0
  26. package/src/components/UserListComponent.jsx +167 -0
  27. package/src/pages/AccountPage.jsx +140 -47
  28. package/src/pages/LoginPage.jsx +6 -5
  29. package/src/pages/PasswordInvitePage.jsx +3 -3
  30. package/src/pages/SignUpPage.jsx +3 -3
  31. package/src/utils/authService.js +68 -0
  32. package/src/utils/errors.js +39 -0
  33. package/src/utils/webauthn.js +51 -0
@@ -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 { authApi } from '../auth/authApi';
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 authApi.loginWithRecoveryPassword(
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 authApi.loginWithPassword(identifier, password);
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 authApi.loginWithPasskey();
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) => authApi.startSocialLogin(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 { authApi } from '../auth/authApi';
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 authApi.verifyResetToken(uid, token);
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 authApi.setNewPassword(uid, token, newPassword);
65
+ await setNewPassword(uid, token, newPassword);
66
66
  setSuccessKey(
67
67
  isInvite
68
68
  ? 'Auth.RESET_PASSWORD_SUCCESS_INVITE'
@@ -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 { authApi } from '../auth/authApi';
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 authApi.validateAccessCode(accessCode);
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 authApi.requestInviteWithCode(email, accessCode);
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
+ }