@micha.bigler/ui-core-micha 1.4.23 → 1.4.25

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.
@@ -1,6 +1,6 @@
1
1
  import apiClient from './apiClient';
2
2
  import { HEADLESS_BASE, USERS_BASE, ACCESS_CODES_BASE } from './authConfig';
3
- import { normaliseApiError } from '../utils/errors'; // Beachte den Pfad zu deiner errors.js
3
+ import { normaliseApiError } from '../utils/auth-errors'; // Beachte den Pfad zu deiner errors.js
4
4
  // --- Internal Helper for CSRF ---
5
5
  function getCsrfToken() {
6
6
  if (typeof document === 'undefined' || !document.cookie)
@@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react';
4
4
  import { Box, Typography, Stack, Button, TextField, IconButton, CircularProgress, Alert, List, ListItem, ListItemText, ListItemSecondaryAction, Tooltip, Divider, } from '@mui/material';
5
5
  import DeleteIcon from '@mui/icons-material/Delete';
6
6
  import { useTranslation } from 'react-i18next';
7
- import { fetchPasskeys, registerPasskey, deletePasskey } from '../auth/authApi';
7
+ import { fetchPasskeys, deletePasskey } from '../auth/authApi';
8
8
  import { registerPasskey } from '../utils/authService';
9
9
  import { FEATURES } from '../auth/authConfig';
10
10
  const PasskeysComponent = () => {
@@ -1,42 +1,61 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- import React, { useState, useContext } from 'react';
2
+ import React, { useState, useContext, useEffect } from 'react';
3
3
  import { useNavigate, useLocation } from 'react-router-dom';
4
4
  import { Helmet } from 'react-helmet';
5
- import { Typography, Box, Alert } from '@mui/material';
5
+ import { Box, Alert } from '@mui/material';
6
+ import { useTranslation } from 'react-i18next';
7
+ // Layout & Context
6
8
  import { NarrowPage } from '../layout/PageLayout';
7
9
  import { AuthContext } from '../auth/AuthContext';
10
+ // API & Services (Clean Architecture)
8
11
  import { loginWithRecoveryPassword, loginWithPassword } from '../auth/authApi';
9
12
  import { loginWithPasskey, startSocialLogin } from '../utils/authService';
13
+ // Components
10
14
  import LoginForm from '../components/LoginForm';
11
15
  import MfaLoginComponent from '../components/MfaLoginComponent';
12
- import { useTranslation } from 'react-i18next';
13
16
  export function LoginPage() {
14
17
  const navigate = useNavigate();
15
18
  const location = useLocation();
16
19
  const { login } = useContext(AuthContext);
20
+ const { t } = useTranslation();
21
+ // State
17
22
  const [step, setStep] = useState('credentials'); // 'credentials' | 'mfa'
18
23
  const [submitting, setSubmitting] = useState(false);
19
24
  const [errorKey, setErrorKey] = useState(null);
20
- const [mfaState, setMfaState] = useState(null); // { availableTypes: [...] }
21
- const { t } = useTranslation();
22
- // Read recovery token + prefill email from query params
25
+ const [mfaState, setMfaState] = useState(null); // { availableTypes: [...], identifier }
26
+ // URL Params parsing
23
27
  const params = new URLSearchParams(location.search);
24
28
  const recoveryToken = params.get('recovery');
29
+ // Auto-fill email if provided in URL (UX improvement)
25
30
  const recoveryEmail = params.get('email') || '';
26
- const handleSubmitCredentials = async ({ identifier, password }) => {
31
+ // --- Helper: Central Success Logic ---
32
+ const handleLoginSuccess = (user) => {
27
33
  var _a;
34
+ login(user); // Update Context
35
+ // Check if "Strong Security" is enforced/required but not met
36
+ const requiresExtra = ((_a = user.security_state) === null || _a === void 0 ? void 0 : _a.requires_additional_security) === true;
37
+ if (requiresExtra) {
38
+ navigate('/account?tab=security&from=weak_login');
39
+ }
40
+ else {
41
+ // Standard Redirect (könnte man noch mit ?next=... erweitern)
42
+ navigate('/');
43
+ }
44
+ };
45
+ // --- Handlers ---
46
+ const handleSubmitCredentials = async ({ identifier, password }) => {
28
47
  setErrorKey(null);
29
48
  setSubmitting(true);
30
49
  try {
31
- // Recovery flow: password login via special endpoint, no MFA
50
+ // A) Recovery Flow
32
51
  if (recoveryToken) {
33
52
  const result = await loginWithRecoveryPassword(identifier, password, recoveryToken);
34
- const user = result.user;
35
- login(user);
53
+ // Recovery login implies a specific redirect usually, usually straight to security settings
54
+ login(result.user);
36
55
  navigate('/account?tab=security&from=recovery');
37
56
  return;
38
57
  }
39
- // Normal login flow via headless allauth
58
+ // B) Standard Password Login
40
59
  const result = await loginWithPassword(identifier, password);
41
60
  if (result.needsMfa) {
42
61
  setMfaState({
@@ -46,15 +65,7 @@ export function LoginPage() {
46
65
  setStep('mfa');
47
66
  }
48
67
  else {
49
- const user = result.user;
50
- login(user);
51
- const requiresExtra = ((_a = user.security_state) === null || _a === void 0 ? void 0 : _a.requires_additional_security) === true;
52
- if (requiresExtra) {
53
- navigate('/account?tab=security&from=weak_login');
54
- }
55
- else {
56
- navigate('/');
57
- }
68
+ handleLoginSuccess(result.user);
58
69
  }
59
70
  }
60
71
  catch (err) {
@@ -65,37 +76,32 @@ export function LoginPage() {
65
76
  }
66
77
  };
67
78
  const handlePasskeyLoginInitial = async () => {
68
- var _a;
69
79
  setErrorKey(null);
70
80
  setSubmitting(true);
71
81
  try {
82
+ // Service handles browser interaction + API calls
72
83
  const { user } = await loginWithPasskey();
73
- login(user);
74
- const requiresExtra = ((_a = user.security_state) === null || _a === void 0 ? void 0 : _a.requires_additional_security) === true;
75
- if (requiresExtra) {
76
- navigate('/account?tab=security&from=weak_login');
77
- }
78
- else {
79
- navigate('/');
80
- }
84
+ handleLoginSuccess(user);
81
85
  }
82
86
  catch (err) {
83
- setErrorKey(err.code || 'Auth.PASSKEY_FAILED');
87
+ // 'Auth.PASSKEY_CANCELLED' is generic, maybe ignore visually or show specific hint
88
+ if (err.code !== 'Auth.PASSKEY_CANCELLED') {
89
+ setErrorKey(err.code || 'Auth.PASSKEY_FAILED');
90
+ }
84
91
  }
85
92
  finally {
86
93
  setSubmitting(false);
87
94
  }
88
95
  };
89
- const handleSocialLogin = (provider) => startSocialLogin(provider);
90
- const handleSignUp = () => navigate('/signup');
91
- const handleForgotPassword = () => navigate('/reset-request-password');
92
96
  const handleMfaSuccess = ({ user, method }) => {
93
- login(user);
97
+ // MFA component should return the user object after verifying code
94
98
  if (method === 'recovery_code') {
99
+ // Recovery codes often trigger a security check prompt
100
+ login(user);
95
101
  navigate('/account?tab=security&from=recovery');
96
102
  }
97
103
  else {
98
- navigate('/');
104
+ handleLoginSuccess(user);
99
105
  }
100
106
  };
101
107
  const handleMfaCancel = () => {
@@ -103,6 +109,7 @@ export function LoginPage() {
103
109
  setMfaState(null);
104
110
  setErrorKey(null);
105
111
  };
106
- return (_jsxs(NarrowPage, { title: t('Auth.PAGE_LOGIN_TITLE'), subtitle: t('Auth.PAGE_LOGIN_SUBTITLE'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t('Auth.PAGE_LOGIN_TITLE')] }) }), errorKey && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(errorKey) })), recoveryToken && !errorKey && (_jsx(Alert, { severity: "info", sx: { mb: 2 }, children: t('Auth.RECOVERY_LOGIN_WARNING', 'Your recovery link was validated. Please sign in with your password to continue.') })), step === 'credentials' && (_jsx(LoginForm, { onSubmit: handleSubmitCredentials, onForgotPassword: handleForgotPassword, onSocialLogin: handleSocialLogin, onPasskeyLogin: handlePasskeyLoginInitial, onSignUp: handleSignUp, disabled: submitting, initialIdentifier: recoveryEmail })), step === 'mfa' && mfaState && (_jsx(Box, { children: _jsx(MfaLoginComponent, { availableTypes: mfaState.availableTypes, identifier: mfaState.identifier, onSuccess: handleMfaSuccess, onCancel: handleMfaCancel }) }))] }));
112
+ // --- Render ---
113
+ return (_jsxs(NarrowPage, { title: t('Auth.PAGE_LOGIN_TITLE'), subtitle: t('Auth.PAGE_LOGIN_SUBTITLE'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t('Auth.PAGE_LOGIN_TITLE')] }) }), errorKey && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(errorKey) })), recoveryToken && !errorKey && (_jsx(Alert, { severity: "info", sx: { mb: 2 }, children: t('Auth.RECOVERY_LOGIN_WARNING', 'Recovery link validated. Please enter your password.') })), step === 'credentials' && (_jsx(LoginForm, { onSubmit: handleSubmitCredentials, onForgotPassword: () => navigate('/reset-request-password'), onSocialLogin: (provider) => startSocialLogin(provider), onPasskeyLogin: handlePasskeyLoginInitial, onSignUp: () => navigate('/signup'), disabled: submitting, initialIdentifier: recoveryEmail })), step === 'mfa' && mfaState && (_jsx(Box, { children: _jsx(MfaLoginComponent, { availableTypes: mfaState.availableTypes, identifier: mfaState.identifier, onSuccess: handleMfaSuccess, onCancel: handleMfaCancel }) }))] }));
107
114
  }
108
115
  export default LoginPage;
@@ -1,4 +1,4 @@
1
- // src/utils/errors.js
1
+ // src/utils/auth-errors.js
2
2
  export function extractErrorInfo(error) {
3
3
  var _a, _b, _c, _d;
4
4
  const status = (_b = (_a = error.response) === null || _a === void 0 ? void 0 : _a.status) !== null && _b !== void 0 ? _b : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "1.4.23",
3
+ "version": "1.4.25",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "private": false,
@@ -1,6 +1,6 @@
1
1
  import apiClient from './apiClient';
2
2
  import { HEADLESS_BASE, USERS_BASE, ACCESS_CODES_BASE } from './authConfig';
3
- import { normaliseApiError } from '../utils/errors'; // Beachte den Pfad zu deiner errors.js
3
+ import { normaliseApiError } from '../utils/auth-errors'; // Beachte den Pfad zu deiner errors.js
4
4
 
5
5
  // --- Internal Helper for CSRF ---
6
6
  function getCsrfToken() {
@@ -18,7 +18,7 @@ import {
18
18
  } from '@mui/material';
19
19
  import DeleteIcon from '@mui/icons-material/Delete';
20
20
  import { useTranslation } from 'react-i18next';
21
- import { fetchPasskeys, registerPasskey, deletePasskey } from '../auth/authApi';
21
+ import { fetchPasskeys, deletePasskey } from '../auth/authApi';
22
22
  import { registerPasskey } from '../utils/authService';
23
23
  import { FEATURES } from '../auth/authConfig';
24
24
 
@@ -1,51 +1,74 @@
1
- import React, { useState, useContext } from 'react';
1
+ import React, { useState, useContext, useEffect } from 'react';
2
2
  import { useNavigate, useLocation } from 'react-router-dom';
3
3
  import { Helmet } from 'react-helmet';
4
- import { Typography, Box, Alert } from '@mui/material';
4
+ import { Box, Alert } from '@mui/material';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ // Layout & Context
5
8
  import { NarrowPage } from '../layout/PageLayout';
6
9
  import { AuthContext } from '../auth/AuthContext';
10
+
11
+ // API & Services (Clean Architecture)
7
12
  import { loginWithRecoveryPassword, loginWithPassword } from '../auth/authApi';
8
13
  import { loginWithPasskey, startSocialLogin } from '../utils/authService';
14
+
15
+ // Components
9
16
  import LoginForm from '../components/LoginForm';
10
17
  import MfaLoginComponent from '../components/MfaLoginComponent';
11
- import { useTranslation } from 'react-i18next';
12
18
 
13
19
  export function LoginPage() {
14
20
  const navigate = useNavigate();
15
21
  const location = useLocation();
16
22
  const { login } = useContext(AuthContext);
23
+ const { t } = useTranslation();
17
24
 
25
+ // State
18
26
  const [step, setStep] = useState('credentials'); // 'credentials' | 'mfa'
19
27
  const [submitting, setSubmitting] = useState(false);
20
28
  const [errorKey, setErrorKey] = useState(null);
21
- const [mfaState, setMfaState] = useState(null); // { availableTypes: [...] }
22
-
23
- const { t } = useTranslation();
29
+ const [mfaState, setMfaState] = useState(null); // { availableTypes: [...], identifier }
24
30
 
25
- // Read recovery token + prefill email from query params
31
+ // URL Params parsing
26
32
  const params = new URLSearchParams(location.search);
27
33
  const recoveryToken = params.get('recovery');
34
+ // Auto-fill email if provided in URL (UX improvement)
28
35
  const recoveryEmail = params.get('email') || '';
29
36
 
37
+ // --- Helper: Central Success Logic ---
38
+ const handleLoginSuccess = (user) => {
39
+ login(user); // Update Context
40
+
41
+ // Check if "Strong Security" is enforced/required but not met
42
+ const requiresExtra = user.security_state?.requires_additional_security === true;
43
+
44
+ if (requiresExtra) {
45
+ navigate('/account?tab=security&from=weak_login');
46
+ } else {
47
+ // Standard Redirect (könnte man noch mit ?next=... erweitern)
48
+ navigate('/');
49
+ }
50
+ };
51
+
52
+ // --- Handlers ---
53
+
30
54
  const handleSubmitCredentials = async ({ identifier, password }) => {
31
55
  setErrorKey(null);
32
56
  setSubmitting(true);
33
57
  try {
34
- // Recovery flow: password login via special endpoint, no MFA
58
+ // A) Recovery Flow
35
59
  if (recoveryToken) {
36
60
  const result = await loginWithRecoveryPassword(
37
61
  identifier,
38
62
  password,
39
- recoveryToken,
63
+ recoveryToken
40
64
  );
41
-
42
- const user = result.user;
43
- login(user);
65
+ // Recovery login implies a specific redirect usually, usually straight to security settings
66
+ login(result.user);
44
67
  navigate('/account?tab=security&from=recovery');
45
68
  return;
46
69
  }
47
70
 
48
- // Normal login flow via headless allauth
71
+ // B) Standard Password Login
49
72
  const result = await loginWithPassword(identifier, password);
50
73
 
51
74
  if (result.needsMfa) {
@@ -55,17 +78,7 @@ export function LoginPage() {
55
78
  });
56
79
  setStep('mfa');
57
80
  } else {
58
- const user = result.user;
59
- login(user);
60
-
61
- const requiresExtra =
62
- user.security_state?.requires_additional_security === true;
63
-
64
- if (requiresExtra) {
65
- navigate('/account?tab=security&from=weak_login');
66
- } else {
67
- navigate('/');
68
- }
81
+ handleLoginSuccess(result.user);
69
82
  }
70
83
  } catch (err) {
71
84
  setErrorKey(err.code || 'Auth.LOGIN_FAILED');
@@ -78,35 +91,27 @@ export function LoginPage() {
78
91
  setErrorKey(null);
79
92
  setSubmitting(true);
80
93
  try {
94
+ // Service handles browser interaction + API calls
81
95
  const { user } = await loginWithPasskey();
82
- login(user);
83
-
84
- const requiresExtra =
85
- user.security_state?.requires_additional_security === true;
86
-
87
- if (requiresExtra) {
88
- navigate('/account?tab=security&from=weak_login');
89
- } else {
90
- navigate('/');
91
- }
96
+ handleLoginSuccess(user);
92
97
  } catch (err) {
93
- setErrorKey(err.code || 'Auth.PASSKEY_FAILED');
98
+ // 'Auth.PASSKEY_CANCELLED' is generic, maybe ignore visually or show specific hint
99
+ if (err.code !== 'Auth.PASSKEY_CANCELLED') {
100
+ setErrorKey(err.code || 'Auth.PASSKEY_FAILED');
101
+ }
94
102
  } finally {
95
103
  setSubmitting(false);
96
104
  }
97
105
  };
98
106
 
99
- const handleSocialLogin = (provider) => startSocialLogin(provider);
100
- const handleSignUp = () => navigate('/signup');
101
- const handleForgotPassword = () => navigate('/reset-request-password');
102
-
103
107
  const handleMfaSuccess = ({ user, method }) => {
104
- login(user);
105
-
108
+ // MFA component should return the user object after verifying code
106
109
  if (method === 'recovery_code') {
110
+ // Recovery codes often trigger a security check prompt
111
+ login(user);
107
112
  navigate('/account?tab=security&from=recovery');
108
113
  } else {
109
- navigate('/');
114
+ handleLoginSuccess(user);
110
115
  }
111
116
  };
112
117
 
@@ -116,15 +121,15 @@ export function LoginPage() {
116
121
  setErrorKey(null);
117
122
  };
118
123
 
124
+ // --- Render ---
125
+
119
126
  return (
120
127
  <NarrowPage
121
128
  title={t('Auth.PAGE_LOGIN_TITLE')}
122
129
  subtitle={t('Auth.PAGE_LOGIN_SUBTITLE')}
123
130
  >
124
131
  <Helmet>
125
- <title>
126
- {t('App.NAME')} – {t('Auth.PAGE_LOGIN_TITLE')}
127
- </title>
132
+ <title>{t('App.NAME')} – {t('Auth.PAGE_LOGIN_TITLE')}</title>
128
133
  </Helmet>
129
134
 
130
135
  {errorKey && (
@@ -135,20 +140,17 @@ export function LoginPage() {
135
140
 
136
141
  {recoveryToken && !errorKey && (
137
142
  <Alert severity="info" sx={{ mb: 2 }}>
138
- {t(
139
- 'Auth.RECOVERY_LOGIN_WARNING',
140
- 'Your recovery link was validated. Please sign in with your password to continue.',
141
- )}
143
+ {t('Auth.RECOVERY_LOGIN_WARNING', 'Recovery link validated. Please enter your password.')}
142
144
  </Alert>
143
145
  )}
144
146
 
145
147
  {step === 'credentials' && (
146
148
  <LoginForm
147
149
  onSubmit={handleSubmitCredentials}
148
- onForgotPassword={handleForgotPassword}
149
- onSocialLogin={handleSocialLogin}
150
- onPasskeyLogin={handlePasskeyLoginInitial} // Passkey as first factor
151
- onSignUp={handleSignUp}
150
+ onForgotPassword={() => navigate('/reset-request-password')}
151
+ onSocialLogin={(provider) => startSocialLogin(provider)}
152
+ onPasskeyLogin={handlePasskeyLoginInitial}
153
+ onSignUp={() => navigate('/signup')}
152
154
  disabled={submitting}
153
155
  initialIdentifier={recoveryEmail}
154
156
  />
@@ -156,6 +158,7 @@ export function LoginPage() {
156
158
 
157
159
  {step === 'mfa' && mfaState && (
158
160
  <Box>
161
+ {/* Assuming MfaLoginComponent handles the API call to authenticateWithMFA */}
159
162
  <MfaLoginComponent
160
163
  availableTypes={mfaState.availableTypes}
161
164
  identifier={mfaState.identifier}
@@ -168,4 +171,4 @@ export function LoginPage() {
168
171
  );
169
172
  }
170
173
 
171
- export default LoginPage;
174
+ export default LoginPage;
@@ -1,4 +1,4 @@
1
- // src/utils/errors.js
1
+ // src/utils/auth-errors.js
2
2
  export function extractErrorInfo(error) {
3
3
  const status = error.response?.status ?? null;
4
4
  const data = error.response?.data ?? null;
@@ -1,10 +0,0 @@
1
- //src/auth/webauthnclient.jsx
2
- export async function registerPasskeyStart() {
3
- const res = await apiClient.post(`${HEADLESS_BASE}/mfa/webauthn/register/start`, {}, { withCredentials: true });
4
- return res.data; // enthält challenge, rpId etc.
5
- }
6
- export async function registerPasskeyComplete(publicKeyCredential) {
7
- const payload = serializePublicKeyCredential(publicKeyCredential);
8
- const res = await apiClient.post(`${HEADLESS_BASE}/mfa/webauthn/register/complete`, payload, { withCredentials: true });
9
- return res.data;
10
- }
@@ -1,19 +0,0 @@
1
- //src/auth/webauthnclient.jsx
2
- export async function registerPasskeyStart() {
3
- const res = await apiClient.post(
4
- `${HEADLESS_BASE}/mfa/webauthn/register/start`,
5
- {},
6
- { withCredentials: true },
7
- );
8
- return res.data; // enthält challenge, rpId etc.
9
- }
10
-
11
- export async function registerPasskeyComplete(publicKeyCredential) {
12
- const payload = serializePublicKeyCredential(publicKeyCredential);
13
- const res = await apiClient.post(
14
- `${HEADLESS_BASE}/mfa/webauthn/register/complete`,
15
- payload,
16
- { withCredentials: true },
17
- );
18
- return res.data;
19
- }