@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.
- package/dist/auth/AuthContext.js +89 -0
- package/dist/auth/authApi.js +172 -0
- package/dist/auth/authConfig.js +23 -0
- package/dist/components/LoginForm.js +22 -0
- package/dist/components/PasswordChangeForm.js +33 -0
- package/dist/components/PasswordResetRequestForm.js +15 -0
- package/dist/components/PasswordSetForm.js +30 -0
- package/dist/components/ProfileComponent.js +104 -0
- package/dist/components/SecurityComponent.js +38 -0
- package/dist/components/SocialLoginButtons.js +35 -0
- package/dist/index.js +14 -0
- package/dist/layout/PageLayout.js +8 -0
- package/dist/pages/AccountPage.js +26 -0
- package/dist/pages/LoginPage.js +50 -0
- package/dist/pages/PasswordChangePage.js +31 -0
- package/dist/pages/PasswordInvitePage.js +68 -0
- package/dist/pages/PasswordResetRequestPage.js +35 -0
- package/package.json +23 -0
- package/src/auth/AuthContext.jsx +108 -0
- package/src/auth/authApi.jsx +211 -0
- package/src/auth/authConfig.jsx +31 -0
- package/src/components/LoginForm.jsx +93 -0
- package/src/components/PasswordChangeForm.jsx +67 -0
- package/src/components/PasswordResetRequestForm.jsx +41 -0
- package/src/components/PasswordSetForm.jsx +76 -0
- package/src/components/ProfileComponent.jsx +230 -0
- package/src/components/SecurityComponent.jsx +96 -0
- package/src/components/SocialLoginButtons.jsx +72 -0
- package/src/index.js +17 -0
- package/src/layout/PageLayout.jsx +34 -0
- package/src/pages/AccountPage.jsx +65 -0
- package/src/pages/LoginPage.jsx +72 -0
- package/src/pages/PasswordChangePage.jsx +58 -0
- package/src/pages/PasswordInvitePage.jsx +108 -0
- package/src/pages/PasswordResetRequestPage.jsx +61 -0
- 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;
|