@micha.bigler/ui-core-micha 1.0.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.
Files changed (36) hide show
  1. package/dist/auth/AuthContext.js +89 -0
  2. package/dist/auth/authApi.js +172 -0
  3. package/dist/auth/authConfig.js +23 -0
  4. package/dist/components/LoginForm.js +22 -0
  5. package/dist/components/PasswordChangeForm.js +33 -0
  6. package/dist/components/PasswordResetRequestForm.js +15 -0
  7. package/dist/components/PasswordSetForm.js +30 -0
  8. package/dist/components/ProfileComponent.js +104 -0
  9. package/dist/components/SecurityComponent.js +38 -0
  10. package/dist/components/SocialLoginButtons.js +35 -0
  11. package/dist/index.js +14 -0
  12. package/dist/layout/PageLayout.js +8 -0
  13. package/dist/pages/AccountPage.js +26 -0
  14. package/dist/pages/LoginPage.js +50 -0
  15. package/dist/pages/PasswordChangePage.js +31 -0
  16. package/dist/pages/PasswordInvitePage.js +68 -0
  17. package/dist/pages/PasswordResetRequestPage.js +35 -0
  18. package/package.json +23 -0
  19. package/src/auth/AuthContext.jsx +108 -0
  20. package/src/auth/authApi.jsx +211 -0
  21. package/src/auth/authConfig.jsx +31 -0
  22. package/src/components/LoginForm.jsx +93 -0
  23. package/src/components/PasswordChangeForm.jsx +67 -0
  24. package/src/components/PasswordResetRequestForm.jsx +41 -0
  25. package/src/components/PasswordSetForm.jsx +76 -0
  26. package/src/components/ProfileComponent.jsx +230 -0
  27. package/src/components/SecurityComponent.jsx +96 -0
  28. package/src/components/SocialLoginButtons.jsx +72 -0
  29. package/src/index.js +17 -0
  30. package/src/layout/PageLayout.jsx +34 -0
  31. package/src/pages/AccountPage.jsx +65 -0
  32. package/src/pages/LoginPage.jsx +72 -0
  33. package/src/pages/PasswordChangePage.jsx +58 -0
  34. package/src/pages/PasswordInvitePage.jsx +108 -0
  35. package/src/pages/PasswordResetRequestPage.jsx +61 -0
  36. package/tsconfig.build.json +16 -0
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useState, useContext } from 'react';
3
+ import { useNavigate } from 'react-router-dom';
4
+ import { Helmet } from 'react-helmet';
5
+ import { Typography } from '@mui/material';
6
+ import { NarrowPage } from '../layout/PageLayout';
7
+ import { AuthContext } from '../auth/AuthContext';
8
+ import { authApi } from '../auth/authApi';
9
+ import LoginForm from '../components/LoginForm';
10
+ export function LoginPage() {
11
+ const navigate = useNavigate();
12
+ const { login } = useContext(AuthContext);
13
+ const [submitting, setSubmitting] = useState(false);
14
+ const [error, setError] = useState('');
15
+ const handleSubmit = async ({ identifier, password }) => {
16
+ var _a, _b;
17
+ setError('');
18
+ setSubmitting(true);
19
+ try {
20
+ const data = await authApi.loginWithPassword(identifier, password);
21
+ login(data.user);
22
+ navigate('/');
23
+ }
24
+ catch (err) {
25
+ setError(((_b = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.detail) ||
26
+ (err === null || err === void 0 ? void 0 : err.message) ||
27
+ 'Login failed.');
28
+ }
29
+ finally {
30
+ setSubmitting(false);
31
+ }
32
+ };
33
+ const handleForgotPassword = () => {
34
+ navigate('/reset-request-password');
35
+ };
36
+ const handleSocialLogin = (providerKey) => {
37
+ setError('');
38
+ try {
39
+ authApi.startSocialLogin(providerKey);
40
+ }
41
+ catch (err) {
42
+ // This will only fire on immediate client-side errors
43
+ console.error('Social login init failed', err);
44
+ setError('Could not start social login.');
45
+ }
46
+ };
47
+ return (_jsxs(NarrowPage, { title: "Login", children: [_jsx(Helmet, { children: _jsx("title", { children: "PROJECT_NAME \u2013 Login" }) }), error && (_jsx(Typography, { color: "error", gutterBottom: true, children: error })), _jsx(LoginForm, { onSubmit: handleSubmit, onForgotPassword: handleForgotPassword, onSocialLogin: handleSocialLogin, error: undefined, disabled: submitting })] }));
48
+ }
49
+ ;
50
+ export default LoginPage;
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // src/auth/pages/PasswordChangePage.jsx
3
+ import React, { useState } from 'react';
4
+ import { Helmet } from 'react-helmet';
5
+ import { Typography } from '@mui/material';
6
+ import { NarrowPage } from '../layout/PageLayout';
7
+ import PasswordChangeForm from '../components/PasswordChangeForm';
8
+ import { changePassword } from '../auth/authApi';
9
+ export function PasswordChangePage() {
10
+ const [submitting, setSubmitting] = useState(false);
11
+ const [successMessage, setSuccessMessage] = useState('');
12
+ const [error, setError] = useState('');
13
+ const handleSubmit = async (oldPassword, newPassword) => {
14
+ setSuccessMessage('');
15
+ setError('');
16
+ setSubmitting(true);
17
+ try {
18
+ await changePassword(oldPassword, newPassword);
19
+ setSuccessMessage('Password changed successfully.');
20
+ }
21
+ catch (err) {
22
+ setError(err.message || 'Could not change password.');
23
+ }
24
+ finally {
25
+ setSubmitting(false);
26
+ }
27
+ };
28
+ return (_jsxs(NarrowPage, { title: "Change password", subtitle: "Enter your current password and a new password.", children: [_jsx(Helmet, { children: _jsx("title", { children: "PROJECT_NAME \u2013 Change password" }) }), successMessage && (_jsx(Typography, { color: "primary", gutterBottom: true, children: successMessage })), error && (_jsx(Typography, { color: "error", gutterBottom: true, children: error })), _jsx(PasswordChangeForm, { onSubmit: handleSubmit, submitting: submitting })] }));
29
+ }
30
+ ;
31
+ export default PasswordChangePage;
@@ -0,0 +1,68 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // src/auth/pages/PasswordInvitePage.jsx
3
+ import React, { useState, useEffect } from 'react';
4
+ import { useNavigate, useParams, useLocation } from 'react-router-dom';
5
+ import { Helmet } from 'react-helmet';
6
+ import { Typography } from '@mui/material';
7
+ import { NarrowPage } from '../layout/PageLayout';
8
+ import PasswordSetForm from '../components/PasswordSetForm';
9
+ import { authApi } from '../auth/authApi';
10
+ export function PasswordInvitePage() {
11
+ const { uid, token } = useParams();
12
+ const location = useLocation();
13
+ const navigate = useNavigate();
14
+ const [submitting, setSubmitting] = useState(false);
15
+ const [error, setError] = useState('');
16
+ const [successMessage, setSuccessMessage] = useState('');
17
+ const [checked, setChecked] = useState(false);
18
+ // Optional: andere Texte je nach „invite“ vs „reset“
19
+ const isInvite = location.pathname.startsWith('/invite/');
20
+ useEffect(() => {
21
+ if (!uid || !token) {
22
+ setError('This link is invalid.');
23
+ setChecked(true);
24
+ return;
25
+ }
26
+ const check = async () => {
27
+ try {
28
+ await authApi.verifyResetToken(uid, token);
29
+ setChecked(true);
30
+ }
31
+ catch (err) {
32
+ setError('This link is invalid or has expired.');
33
+ setChecked(true);
34
+ }
35
+ };
36
+ check();
37
+ }, [uid, token]);
38
+ const handleSubmit = async (newPassword) => {
39
+ if (!uid || !token) {
40
+ setError('This link is invalid.');
41
+ return;
42
+ }
43
+ setSubmitting(true);
44
+ setError('');
45
+ setSuccessMessage('');
46
+ try {
47
+ await authApi.setNewPassword(uid, token, newPassword);
48
+ setSuccessMessage(isInvite
49
+ ? 'Password set successfully. You can now log in.'
50
+ : 'Password changed successfully. You can now log in.');
51
+ navigate('/login');
52
+ }
53
+ catch (err) {
54
+ setError(err.message || 'Could not set password.');
55
+ }
56
+ finally {
57
+ setSubmitting(false);
58
+ }
59
+ };
60
+ if (!checked && !error) {
61
+ return (_jsx(NarrowPage, { title: "Checking link\u2026", children: _jsx(Typography, { children: "Validating your link\u2026" }) }));
62
+ }
63
+ return (_jsxs(NarrowPage, { title: isInvite ? 'Welcome' : 'Reset password', subtitle: isInvite
64
+ ? 'Please choose a password to access your account.'
65
+ : 'Please choose a new password.', children: [_jsx(Helmet, { children: _jsx("title", { children: "PROJECT_NAME \u2013 Set password" }) }), error && (_jsx(Typography, { color: "error", gutterBottom: true, children: error })), successMessage && (_jsx(Typography, { color: "primary", gutterBottom: true, children: successMessage })), !successMessage && !error && (_jsx(PasswordSetForm, { onSubmit: handleSubmit, submitting: submitting }))] }));
66
+ }
67
+ ;
68
+ export default PasswordInvitePage;
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // src/auth/pages/PasswordResetRequestPage.jsx
3
+ import React, { useState } from 'react';
4
+ import { Helmet } from 'react-helmet';
5
+ import { Typography } from '@mui/material';
6
+ import { NarrowPage } from '../layout/PageLayout';
7
+ import { requestPasswordReset } from '../auth/authApi';
8
+ import PasswordResetRequestForm from '../components/PasswordResetRequestForm';
9
+ export function PasswordResetRequestPage() {
10
+ const [submitting, setSubmitting] = useState(false);
11
+ const [successMessage, setSuccessMessage] = useState('');
12
+ const [error, setError] = useState('');
13
+ const handleSubmit = async (email) => {
14
+ setSuccessMessage('');
15
+ setError('');
16
+ if (!email) {
17
+ setError('Please enter an email address.');
18
+ return;
19
+ }
20
+ setSubmitting(true);
21
+ try {
22
+ await requestPasswordReset(email);
23
+ setSuccessMessage('If an account exists for this address, a reset email has been sent.');
24
+ }
25
+ catch (err) {
26
+ setError(err.message || 'Could not send reset email.');
27
+ }
28
+ finally {
29
+ setSubmitting(false);
30
+ }
31
+ };
32
+ return (_jsxs(NarrowPage, { title: "Reset password", children: [_jsx(Helmet, { children: _jsx("title", { children: "PROJECT_NAME \u2013 Reset password" }) }), successMessage && (_jsx(Typography, { colour: "primary", gutterBottom: true, children: successMessage })), error && (_jsx(Typography, { colour: "error", gutterBottom: true, children: error })), _jsx(PasswordResetRequestForm, { onSubmit: handleSubmit, submitting: submitting })] }));
33
+ }
34
+ ;
35
+ export default PasswordResetRequestPage;
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@micha.bigler/ui-core-micha",
3
+ "version": "1.0.0",
4
+ "main": "dist/index.js",
5
+ "module": "dist/index.js",
6
+ "private": false,
7
+ "peerDependencies": {
8
+ "@emotion/react": "^11.0.0",
9
+ "@emotion/styled": "^11.0.0",
10
+ "@mui/material": "^5.0.0",
11
+ "axios": "^1.0.0",
12
+ "react": "^18.0.0",
13
+ "react-dom": "^18.0.0",
14
+ "react-helmet": "^6.0.0",
15
+ "react-router-dom": "^6.0.0"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.build.json"
19
+ },
20
+ "devDependencies": {
21
+ "typescript": "^5.9.3"
22
+ }
23
+ }
@@ -0,0 +1,108 @@
1
+ // src/auth/AuthContext.jsx
2
+ import React, {
3
+ createContext,
4
+ useState,
5
+ useEffect,
6
+ } from 'react';
7
+ import axios from 'axios';
8
+ import { CSRF_URL } from './authConfig';
9
+ import {
10
+ fetchCurrentUser,
11
+ logoutSession,
12
+ } from './authApi';
13
+
14
+ export const AuthContext = createContext(null);
15
+
16
+ export const AuthProvider = ({ children }) => {
17
+ const [user, setUser] = useState(null);
18
+ const [loading, setLoading] = useState(true);
19
+
20
+ // Einmalige Axios-Basis-Konfiguration
21
+ useEffect(() => {
22
+ axios.defaults.withCredentials = true;
23
+ axios.defaults.xsrfCookieName = 'csrftoken';
24
+ axios.defaults.xsrfHeaderName = 'X-CSRFToken';
25
+ }, []);
26
+
27
+ useEffect(() => {
28
+ let isMounted = true;
29
+
30
+ const initAuth = async () => {
31
+ try {
32
+ // 1) CSRF-Cookie setzen (Django-View /api/csrf/)
33
+ try {
34
+ await axios.get(CSRF_URL, { withCredentials: true });
35
+ // console.log('CSRF cookie set');
36
+ } catch (err) {
37
+ // eslint-disable-next-line no-console
38
+ console.error('Error setting CSRF cookie:', err);
39
+ }
40
+
41
+ // 2) aktuellen User laden (falls Session vorhanden)
42
+ try {
43
+ const data = await fetchCurrentUser();
44
+ if (!isMounted) return;
45
+ setUser({
46
+ id: data.id,
47
+ username: data.username,
48
+ email: data.email,
49
+ first_name: data.first_name,
50
+ last_name: data.last_name,
51
+ role: data.role,
52
+ is_superuser: data.is_superuser,
53
+ });
54
+ } catch (err) {
55
+ // Kein eingeloggter User ist ein normaler Fall
56
+ // eslint-disable-next-line no-console
57
+ console.log('No logged-in user:', err?.message || err);
58
+ if (!isMounted) return;
59
+ setUser(null);
60
+ }
61
+ } finally {
62
+ if (isMounted) {
63
+ setLoading(false);
64
+ }
65
+ }
66
+ };
67
+
68
+ initAuth();
69
+
70
+ return () => {
71
+ isMounted = false;
72
+ };
73
+ }, []);
74
+
75
+ // Nach erfolgreichem Login das User-Objekt setzen
76
+ // (z. B. aus loginWithPassword in authApi)
77
+ const login = (userData) => {
78
+ setUser((prev) => ({
79
+ ...prev,
80
+ ...userData,
81
+ }));
82
+ };
83
+
84
+ // Logout im Backend + lokalen State leeren
85
+ const logout = async () => {
86
+ try {
87
+ await logoutSession();
88
+ } catch (error) {
89
+ // eslint-disable-next-line no-console
90
+ console.error('Error during logout:', error);
91
+ } finally {
92
+ setUser(null);
93
+ }
94
+ };
95
+
96
+ return (
97
+ <AuthContext.Provider
98
+ value={{
99
+ user,
100
+ loading,
101
+ login,
102
+ logout,
103
+ }}
104
+ >
105
+ {children}
106
+ </AuthContext.Provider>
107
+ );
108
+ };
@@ -0,0 +1,211 @@
1
+ import axios from 'axios';
2
+ import { HEADLESS_BASE, USERS_BASE } from './authConfig';
3
+
4
+ // Helper to normalise error messages from API responses
5
+ function extractErrorMessage(error) {
6
+ const data = error.response?.data;
7
+ if (!data) {
8
+ return error.message || 'Unknown error';
9
+ }
10
+ if (typeof data.detail === 'string') {
11
+ return data.detail;
12
+ }
13
+ if (Array.isArray(data.non_field_errors) && data.non_field_errors.length > 0) {
14
+ return data.non_field_errors[0];
15
+ }
16
+ return JSON.stringify(data);
17
+ }
18
+
19
+ // Helper to get CSRF token from cookies manually
20
+ function getCsrfToken() {
21
+ if (!document.cookie) return null;
22
+ const match = document.cookie.match(/csrftoken=([^;]+)/);
23
+ return match ? match[1] : null;
24
+ }
25
+
26
+ /**
27
+ * Fetches the current authenticated user from your own User API.
28
+ */
29
+ export async function fetchCurrentUser() {
30
+ const res = await axios.get(`${USERS_BASE}/current/`, {
31
+ withCredentials: true,
32
+ });
33
+ return res.data;
34
+ }
35
+
36
+ /**
37
+ * Updates the user profile fields.
38
+ */
39
+ export async function updateUserProfile(data) {
40
+ const res = await axios.patch(`${USERS_BASE}/current/`, data, {
41
+ withCredentials: true,
42
+ });
43
+ return res.data;
44
+ }
45
+
46
+ /**
47
+ * Logs a user in using email/password via allauth headless.
48
+ */
49
+ export async function loginWithPassword(email, password) {
50
+ try {
51
+ await axios.post(
52
+ `${HEADLESS_BASE}/auth/login`,
53
+ {
54
+ email,
55
+ password,
56
+ },
57
+ { withCredentials: true },
58
+ );
59
+ } catch (error) {
60
+ if (error.response && error.response.status === 409) {
61
+ // Proceed normally if already logged in
62
+ } else {
63
+ throw new Error(extractErrorMessage(error));
64
+ }
65
+ }
66
+
67
+ const user = await fetchCurrentUser();
68
+ return user;
69
+ }
70
+
71
+ /**
72
+ * Requests a password reset email via allauth headless.
73
+ */
74
+ export async function requestPasswordReset(email) {
75
+ try {
76
+ await axios.post(
77
+ `${USERS_BASE}/reset-request/`,
78
+ { email },
79
+ { withCredentials: true },
80
+ );
81
+ } catch (error) {
82
+ throw new Error(extractErrorMessage(error));
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Sets a new password using a reset key (from email link).
88
+ */
89
+ export async function resetPasswordWithKey(key, newPassword) {
90
+ try {
91
+ await axios.post(
92
+ `${HEADLESS_BASE}/auth/password/reset/key`,
93
+ {
94
+ key,
95
+ password: newPassword,
96
+ },
97
+ { withCredentials: true },
98
+ );
99
+ } catch (error) {
100
+ throw new Error(extractErrorMessage(error));
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Changes the password for an authenticated user.
106
+ */
107
+ export async function changePassword(currentPassword, newPassword) {
108
+ try {
109
+ await axios.post(
110
+ `${HEADLESS_BASE}/account/password/change`,
111
+ {
112
+ current_password: currentPassword,
113
+ new_password: newPassword,
114
+ },
115
+ { withCredentials: true },
116
+ );
117
+ } catch (error) {
118
+ throw new Error(extractErrorMessage(error));
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Logs the user out via allauth headless.
124
+ */
125
+ export async function logoutSession() {
126
+ try {
127
+ const headers = {};
128
+ const csrfToken = getCsrfToken();
129
+ if (csrfToken) {
130
+ headers['X-CSRFToken'] = csrfToken;
131
+ }
132
+
133
+ await axios.delete(
134
+ `${HEADLESS_BASE}/auth/session`,
135
+ {
136
+ withCredentials: true,
137
+ headers,
138
+ },
139
+ );
140
+ } catch (error) {
141
+ if (error.response && [401, 404, 410].includes(error.response.status)) {
142
+ return;
143
+ }
144
+ // eslint-disable-next-line no-console
145
+ console.error('Logout error:', error);
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Starts an OAuth social login flow for the given provider.
151
+ * Provider examples: "google", "microsoft".
152
+ * * FIX:
153
+ * 1. Uses POST instead of GET (standard for headless init flows).
154
+ * 2. Uses the correct path '/providers/{provider}/login'.
155
+ */
156
+ export function startSocialLogin(provider) {
157
+ window.location.href = `/accounts/${provider}/login/?process=login`;
158
+ }
159
+
160
+
161
+
162
+ /**
163
+ * Loads the current session information directly from allauth headless.
164
+ */
165
+ export async function fetchHeadlessSession() {
166
+ const res = await axios.get(`${HEADLESS_BASE}/auth/session`, {
167
+ withCredentials: true,
168
+ });
169
+ return res.data;
170
+ }
171
+
172
+ export async function loginWithPasskey() {
173
+ throw new Error('Passkey login is not implemented yet.');
174
+ }
175
+
176
+ export async function registerPasskey() {
177
+ throw new Error('Passkey registration is not implemented yet.');
178
+ }
179
+
180
+ export async function verifyResetToken(uid, token) {
181
+ const res = await axios.get(
182
+ `${USERS_BASE}/password-reset/${uid}/${token}/`,
183
+ { withCredentials: true },
184
+ );
185
+ return res.data;
186
+ }
187
+
188
+ export async function setNewPassword(uid, token, newPassword) {
189
+ const res = await axios.post(
190
+ `${USERS_BASE}/password-reset/${uid}/${token}/`,
191
+ { new_password: newPassword },
192
+ { withCredentials: true },
193
+ );
194
+ return res.data;
195
+ }
196
+
197
+
198
+ export const authApi = {
199
+ fetchCurrentUser,
200
+ updateUserProfile,
201
+ loginWithPassword,
202
+ requestPasswordReset,
203
+ changePassword,
204
+ logoutSession,
205
+ startSocialLogin,
206
+ fetchHeadlessSession,
207
+ verifyResetToken,
208
+ setNewPassword,
209
+ loginWithPasskey,
210
+ registerPasskey,
211
+ };
@@ -0,0 +1,31 @@
1
+ // src/auth/authConfig.js
2
+
3
+ // Basis-Pfad für die Headless-Allauth-API
4
+ export const AUTH_BASE = '/api/auth';
5
+
6
+ // Variante (z. B. "browser" oder später "app")
7
+ export const HEADLESS_VARIANT = 'browser';
8
+
9
+ // Version der Headless-API
10
+ export const HEADLESS_VERSION = 'v1';
11
+
12
+ // Vollständige Basis-URL für Auth-Calls zu allauth.headless
13
+ export const HEADLESS_BASE = `${AUTH_BASE}/${HEADLESS_VARIANT}/${HEADLESS_VERSION}`;
14
+
15
+ // Eigene User-API
16
+ export const USERS_BASE = '/api/users';
17
+
18
+ // CSRF-Endpoint (Django-View csrf_token_view)
19
+ export const CSRF_URL = '/api/csrf/';
20
+
21
+ // Konfiguration der Social-Provider
22
+ export const SOCIAL_PROVIDERS = {
23
+ google: 'google',
24
+ microsoft: 'microsoft',
25
+ };
26
+
27
+ // Feature-Flags (falls du später Dinge toggeln willst)
28
+ export const FEATURES = {
29
+ passkeysEnabled: false, // kannst du später auf true setzen
30
+ mfaEnabled: true,
31
+ };
@@ -0,0 +1,93 @@
1
+ // src/auth/components/LoginForm.jsx
2
+ import React, { useState } from 'react';
3
+ import {
4
+ Box,
5
+ TextField,
6
+ Button,
7
+ Typography,
8
+ Divider,
9
+ } from '@mui/material';
10
+ import SocialLoginButtons from './SocialLoginButtons';
11
+
12
+ const LoginForm = ({
13
+ onSubmit,
14
+ onForgotPassword,
15
+ onSocialLogin,
16
+ error,
17
+ disabled = false,
18
+ }) => {
19
+ const [identifier, setIdentifier] = useState('');
20
+ const [password, setPassword] = useState('');
21
+
22
+ const handleSubmit = (event) => {
23
+ event.preventDefault();
24
+ if (!onSubmit) return;
25
+ onSubmit({ identifier, password });
26
+ };
27
+
28
+ return (
29
+ <Box
30
+ component="form"
31
+ onSubmit={handleSubmit}
32
+ sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}
33
+ >
34
+ {error && (
35
+ <Typography color="error" gutterBottom>
36
+ {error}
37
+ </Typography>
38
+ )}
39
+
40
+ <TextField
41
+ label="Email address"
42
+ type="email"
43
+ required
44
+ fullWidth
45
+ value={identifier}
46
+ onChange={(e) => setIdentifier(e.target.value)}
47
+ disabled={disabled}
48
+ />
49
+
50
+ <TextField
51
+ label="Password"
52
+ type="password"
53
+ required
54
+ fullWidth
55
+ value={password}
56
+ onChange={(e) => setPassword(e.target.value)}
57
+ disabled={disabled}
58
+ />
59
+
60
+ <Box
61
+ sx={{
62
+ display: 'flex',
63
+ justifyContent: 'space-between',
64
+ alignItems: 'center',
65
+ mt: 1,
66
+ }}
67
+ >
68
+ <Button
69
+ type="submit"
70
+ variant="contained"
71
+ disabled={disabled}
72
+ >
73
+ Login
74
+ </Button>
75
+
76
+ <Button
77
+ type="button"
78
+ variant="text"
79
+ onClick={onForgotPassword}
80
+ disabled={disabled}
81
+ >
82
+ Forgot password?
83
+ </Button>
84
+ </Box>
85
+
86
+ <Divider sx={{ my: 2 }}>or</Divider>
87
+
88
+ <SocialLoginButtons onProviderClick={onSocialLogin} />
89
+ </Box>
90
+ );
91
+ };
92
+
93
+ export default LoginForm;