@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,89 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// src/auth/AuthContext.jsx
|
|
3
|
+
import React, { createContext, useState, useEffect, } from 'react';
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
import { CSRF_URL } from './authConfig';
|
|
6
|
+
import { fetchCurrentUser, logoutSession, } from './authApi';
|
|
7
|
+
export const AuthContext = createContext(null);
|
|
8
|
+
export const AuthProvider = ({ children }) => {
|
|
9
|
+
const [user, setUser] = useState(null);
|
|
10
|
+
const [loading, setLoading] = useState(true);
|
|
11
|
+
// Einmalige Axios-Basis-Konfiguration
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
axios.defaults.withCredentials = true;
|
|
14
|
+
axios.defaults.xsrfCookieName = 'csrftoken';
|
|
15
|
+
axios.defaults.xsrfHeaderName = 'X-CSRFToken';
|
|
16
|
+
}, []);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
let isMounted = true;
|
|
19
|
+
const initAuth = async () => {
|
|
20
|
+
try {
|
|
21
|
+
// 1) CSRF-Cookie setzen (Django-View /api/csrf/)
|
|
22
|
+
try {
|
|
23
|
+
await axios.get(CSRF_URL, { withCredentials: true });
|
|
24
|
+
// console.log('CSRF cookie set');
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
// eslint-disable-next-line no-console
|
|
28
|
+
console.error('Error setting CSRF cookie:', err);
|
|
29
|
+
}
|
|
30
|
+
// 2) aktuellen User laden (falls Session vorhanden)
|
|
31
|
+
try {
|
|
32
|
+
const data = await fetchCurrentUser();
|
|
33
|
+
if (!isMounted)
|
|
34
|
+
return;
|
|
35
|
+
setUser({
|
|
36
|
+
id: data.id,
|
|
37
|
+
username: data.username,
|
|
38
|
+
email: data.email,
|
|
39
|
+
first_name: data.first_name,
|
|
40
|
+
last_name: data.last_name,
|
|
41
|
+
role: data.role,
|
|
42
|
+
is_superuser: data.is_superuser,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
// Kein eingeloggter User ist ein normaler Fall
|
|
47
|
+
// eslint-disable-next-line no-console
|
|
48
|
+
console.log('No logged-in user:', (err === null || err === void 0 ? void 0 : err.message) || err);
|
|
49
|
+
if (!isMounted)
|
|
50
|
+
return;
|
|
51
|
+
setUser(null);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
if (isMounted) {
|
|
56
|
+
setLoading(false);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
initAuth();
|
|
61
|
+
return () => {
|
|
62
|
+
isMounted = false;
|
|
63
|
+
};
|
|
64
|
+
}, []);
|
|
65
|
+
// Nach erfolgreichem Login das User-Objekt setzen
|
|
66
|
+
// (z. B. aus loginWithPassword in authApi)
|
|
67
|
+
const login = (userData) => {
|
|
68
|
+
setUser((prev) => (Object.assign(Object.assign({}, prev), userData)));
|
|
69
|
+
};
|
|
70
|
+
// Logout im Backend + lokalen State leeren
|
|
71
|
+
const logout = async () => {
|
|
72
|
+
try {
|
|
73
|
+
await logoutSession();
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
// eslint-disable-next-line no-console
|
|
77
|
+
console.error('Error during logout:', error);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
setUser(null);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
return (_jsx(AuthContext.Provider, { value: {
|
|
84
|
+
user,
|
|
85
|
+
loading,
|
|
86
|
+
login,
|
|
87
|
+
logout,
|
|
88
|
+
}, children: children }));
|
|
89
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { HEADLESS_BASE, USERS_BASE } from './authConfig';
|
|
3
|
+
// Helper to normalise error messages from API responses
|
|
4
|
+
function extractErrorMessage(error) {
|
|
5
|
+
var _a;
|
|
6
|
+
const data = (_a = error.response) === null || _a === void 0 ? void 0 : _a.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
|
+
// Helper to get CSRF token from cookies manually
|
|
19
|
+
function getCsrfToken() {
|
|
20
|
+
if (!document.cookie)
|
|
21
|
+
return null;
|
|
22
|
+
const match = document.cookie.match(/csrftoken=([^;]+)/);
|
|
23
|
+
return match ? match[1] : null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Fetches the current authenticated user from your own User API.
|
|
27
|
+
*/
|
|
28
|
+
export async function fetchCurrentUser() {
|
|
29
|
+
const res = await axios.get(`${USERS_BASE}/current/`, {
|
|
30
|
+
withCredentials: true,
|
|
31
|
+
});
|
|
32
|
+
return res.data;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Updates the user profile fields.
|
|
36
|
+
*/
|
|
37
|
+
export async function updateUserProfile(data) {
|
|
38
|
+
const res = await axios.patch(`${USERS_BASE}/current/`, data, {
|
|
39
|
+
withCredentials: true,
|
|
40
|
+
});
|
|
41
|
+
return res.data;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Logs a user in using email/password via allauth headless.
|
|
45
|
+
*/
|
|
46
|
+
export async function loginWithPassword(email, password) {
|
|
47
|
+
try {
|
|
48
|
+
await axios.post(`${HEADLESS_BASE}/auth/login`, {
|
|
49
|
+
email,
|
|
50
|
+
password,
|
|
51
|
+
}, { withCredentials: true });
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (error.response && error.response.status === 409) {
|
|
55
|
+
// Proceed normally if already logged in
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
throw new Error(extractErrorMessage(error));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const user = await fetchCurrentUser();
|
|
62
|
+
return user;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Requests a password reset email via allauth headless.
|
|
66
|
+
*/
|
|
67
|
+
export async function requestPasswordReset(email) {
|
|
68
|
+
try {
|
|
69
|
+
await axios.post(`${USERS_BASE}/reset-request/`, { email }, { withCredentials: true });
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
throw new Error(extractErrorMessage(error));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Sets a new password using a reset key (from email link).
|
|
77
|
+
*/
|
|
78
|
+
export async function resetPasswordWithKey(key, newPassword) {
|
|
79
|
+
try {
|
|
80
|
+
await axios.post(`${HEADLESS_BASE}/auth/password/reset/key`, {
|
|
81
|
+
key,
|
|
82
|
+
password: newPassword,
|
|
83
|
+
}, { withCredentials: true });
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
throw new Error(extractErrorMessage(error));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Changes the password for an authenticated user.
|
|
91
|
+
*/
|
|
92
|
+
export async function changePassword(currentPassword, newPassword) {
|
|
93
|
+
try {
|
|
94
|
+
await axios.post(`${HEADLESS_BASE}/account/password/change`, {
|
|
95
|
+
current_password: currentPassword,
|
|
96
|
+
new_password: newPassword,
|
|
97
|
+
}, { withCredentials: true });
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
throw new Error(extractErrorMessage(error));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Logs the user out via allauth headless.
|
|
105
|
+
*/
|
|
106
|
+
export async function logoutSession() {
|
|
107
|
+
try {
|
|
108
|
+
const headers = {};
|
|
109
|
+
const csrfToken = getCsrfToken();
|
|
110
|
+
if (csrfToken) {
|
|
111
|
+
headers['X-CSRFToken'] = csrfToken;
|
|
112
|
+
}
|
|
113
|
+
await axios.delete(`${HEADLESS_BASE}/auth/session`, {
|
|
114
|
+
withCredentials: true,
|
|
115
|
+
headers,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (error.response && [401, 404, 410].includes(error.response.status)) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// eslint-disable-next-line no-console
|
|
123
|
+
console.error('Logout error:', error);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Starts an OAuth social login flow for the given provider.
|
|
128
|
+
* Provider examples: "google", "microsoft".
|
|
129
|
+
* * FIX:
|
|
130
|
+
* 1. Uses POST instead of GET (standard for headless init flows).
|
|
131
|
+
* 2. Uses the correct path '/providers/{provider}/login'.
|
|
132
|
+
*/
|
|
133
|
+
export function startSocialLogin(provider) {
|
|
134
|
+
window.location.href = `/accounts/${provider}/login/?process=login`;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Loads the current session information directly from allauth headless.
|
|
138
|
+
*/
|
|
139
|
+
export async function fetchHeadlessSession() {
|
|
140
|
+
const res = await axios.get(`${HEADLESS_BASE}/auth/session`, {
|
|
141
|
+
withCredentials: true,
|
|
142
|
+
});
|
|
143
|
+
return res.data;
|
|
144
|
+
}
|
|
145
|
+
export async function loginWithPasskey() {
|
|
146
|
+
throw new Error('Passkey login is not implemented yet.');
|
|
147
|
+
}
|
|
148
|
+
export async function registerPasskey() {
|
|
149
|
+
throw new Error('Passkey registration is not implemented yet.');
|
|
150
|
+
}
|
|
151
|
+
export async function verifyResetToken(uid, token) {
|
|
152
|
+
const res = await axios.get(`${USERS_BASE}/password-reset/${uid}/${token}/`, { withCredentials: true });
|
|
153
|
+
return res.data;
|
|
154
|
+
}
|
|
155
|
+
export async function setNewPassword(uid, token, newPassword) {
|
|
156
|
+
const res = await axios.post(`${USERS_BASE}/password-reset/${uid}/${token}/`, { new_password: newPassword }, { withCredentials: true });
|
|
157
|
+
return res.data;
|
|
158
|
+
}
|
|
159
|
+
export const authApi = {
|
|
160
|
+
fetchCurrentUser,
|
|
161
|
+
updateUserProfile,
|
|
162
|
+
loginWithPassword,
|
|
163
|
+
requestPasswordReset,
|
|
164
|
+
changePassword,
|
|
165
|
+
logoutSession,
|
|
166
|
+
startSocialLogin,
|
|
167
|
+
fetchHeadlessSession,
|
|
168
|
+
verifyResetToken,
|
|
169
|
+
setNewPassword,
|
|
170
|
+
loginWithPasskey,
|
|
171
|
+
registerPasskey,
|
|
172
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/auth/authConfig.js
|
|
2
|
+
// Basis-Pfad für die Headless-Allauth-API
|
|
3
|
+
export const AUTH_BASE = '/api/auth';
|
|
4
|
+
// Variante (z. B. "browser" oder später "app")
|
|
5
|
+
export const HEADLESS_VARIANT = 'browser';
|
|
6
|
+
// Version der Headless-API
|
|
7
|
+
export const HEADLESS_VERSION = 'v1';
|
|
8
|
+
// Vollständige Basis-URL für Auth-Calls zu allauth.headless
|
|
9
|
+
export const HEADLESS_BASE = `${AUTH_BASE}/${HEADLESS_VARIANT}/${HEADLESS_VERSION}`;
|
|
10
|
+
// Eigene User-API
|
|
11
|
+
export const USERS_BASE = '/api/users';
|
|
12
|
+
// CSRF-Endpoint (Django-View csrf_token_view)
|
|
13
|
+
export const CSRF_URL = '/api/csrf/';
|
|
14
|
+
// Konfiguration der Social-Provider
|
|
15
|
+
export const SOCIAL_PROVIDERS = {
|
|
16
|
+
google: 'google',
|
|
17
|
+
microsoft: 'microsoft',
|
|
18
|
+
};
|
|
19
|
+
// Feature-Flags (falls du später Dinge toggeln willst)
|
|
20
|
+
export const FEATURES = {
|
|
21
|
+
passkeysEnabled: false, // kannst du später auf true setzen
|
|
22
|
+
mfaEnabled: true,
|
|
23
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// src/auth/components/LoginForm.jsx
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { Box, TextField, Button, Typography, Divider, } from '@mui/material';
|
|
5
|
+
import SocialLoginButtons from './SocialLoginButtons';
|
|
6
|
+
const LoginForm = ({ onSubmit, onForgotPassword, onSocialLogin, error, disabled = false, }) => {
|
|
7
|
+
const [identifier, setIdentifier] = useState('');
|
|
8
|
+
const [password, setPassword] = useState('');
|
|
9
|
+
const handleSubmit = (event) => {
|
|
10
|
+
event.preventDefault();
|
|
11
|
+
if (!onSubmit)
|
|
12
|
+
return;
|
|
13
|
+
onSubmit({ identifier, password });
|
|
14
|
+
};
|
|
15
|
+
return (_jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { display: 'flex', flexDirection: 'column', gap: 2 }, children: [error && (_jsx(Typography, { color: "error", gutterBottom: true, children: error })), _jsx(TextField, { label: "Email address", type: "email", required: true, fullWidth: true, value: identifier, onChange: (e) => setIdentifier(e.target.value), disabled: disabled }), _jsx(TextField, { label: "Password", type: "password", required: true, fullWidth: true, value: password, onChange: (e) => setPassword(e.target.value), disabled: disabled }), _jsxs(Box, { sx: {
|
|
16
|
+
display: 'flex',
|
|
17
|
+
justifyContent: 'space-between',
|
|
18
|
+
alignItems: 'center',
|
|
19
|
+
mt: 1,
|
|
20
|
+
}, children: [_jsx(Button, { type: "submit", variant: "contained", disabled: disabled, children: "Login" }), _jsx(Button, { type: "button", variant: "text", onClick: onForgotPassword, disabled: disabled, children: "Forgot password?" })] }), _jsx(Divider, { sx: { my: 2 }, children: "or" }), _jsx(SocialLoginButtons, { onProviderClick: onSocialLogin })] }));
|
|
21
|
+
};
|
|
22
|
+
export default LoginForm;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import { Box, TextField, Button, Stack } from '@mui/material';
|
|
4
|
+
/**
|
|
5
|
+
* A simplified form to handle password changes.
|
|
6
|
+
* Does not require password confirmation.
|
|
7
|
+
*/
|
|
8
|
+
const PasswordChangeForm = ({ onSubmit }) => {
|
|
9
|
+
const [currentPassword, setCurrentPassword] = useState('');
|
|
10
|
+
const [newPassword, setNewPassword] = useState('');
|
|
11
|
+
const [submitting, setSubmitting] = useState(false);
|
|
12
|
+
const handleSubmit = async (e) => {
|
|
13
|
+
e.preventDefault();
|
|
14
|
+
if (!onSubmit)
|
|
15
|
+
return;
|
|
16
|
+
setSubmitting(true);
|
|
17
|
+
try {
|
|
18
|
+
// Send the current and new password to the parent component
|
|
19
|
+
await onSubmit(currentPassword, newPassword);
|
|
20
|
+
// Reset form fields on success
|
|
21
|
+
setCurrentPassword('');
|
|
22
|
+
setNewPassword('');
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
// Errors are generally handled by the parent
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
setSubmitting(false);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
return (_jsx(Box, { component: "form", onSubmit: handleSubmit, sx: { maxWidth: 500 }, children: _jsxs(Stack, { spacing: 2, children: [_jsx(TextField, { label: "Current Password", type: "password", value: currentPassword, onChange: (e) => setCurrentPassword(e.target.value), required: true, fullWidth: true, disabled: submitting }), _jsx(TextField, { label: "New Password", type: "password", value: newPassword, onChange: (e) => setNewPassword(e.target.value), required: true, fullWidth: true, disabled: submitting }), _jsx(Box, { children: _jsx(Button, { type: "submit", variant: "contained", disabled: submitting, children: submitting ? 'Changing...' : 'Change Password' }) })] }) }));
|
|
32
|
+
};
|
|
33
|
+
export default PasswordChangeForm;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// src/auth/components/PasswordResetRequestForm.jsx
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { Box, TextField, Button } from '@mui/material';
|
|
5
|
+
const PasswordResetRequestForm = ({ onSubmit, submitting = false }) => {
|
|
6
|
+
const [email, setEmail] = useState('');
|
|
7
|
+
const handleSubmit = (event) => {
|
|
8
|
+
event.preventDefault();
|
|
9
|
+
if (onSubmit) {
|
|
10
|
+
onSubmit(email);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
return (_jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { display: 'flex', flexDirection: 'column', gap: 2, mt: 2 }, children: [_jsx(TextField, { label: "Email address", type: "email", required: true, fullWidth: true, value: email, onChange: (e) => setEmail(e.target.value), disabled: submitting }), _jsx(Button, { type: "submit", variant: "contained", disabled: submitting, children: "Send reset link" })] }));
|
|
14
|
+
};
|
|
15
|
+
export default PasswordResetRequestForm;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// src/auth/components/PasswordSetForm.jsx
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { Box, TextField, Button } from '@mui/material';
|
|
5
|
+
/**
|
|
6
|
+
* Simple form to set a new password (once, with confirmation).
|
|
7
|
+
* Caller reicht onSubmit(newPassword), kümmert sich um redirect / API call.
|
|
8
|
+
*/
|
|
9
|
+
const PasswordSetForm = ({ onSubmit, submitting = false }) => {
|
|
10
|
+
const [password1, setPassword1] = useState('');
|
|
11
|
+
const [password2, setPassword2] = useState('');
|
|
12
|
+
const [localError, setLocalError] = useState('');
|
|
13
|
+
const handleSubmit = (event) => {
|
|
14
|
+
event.preventDefault();
|
|
15
|
+
setLocalError('');
|
|
16
|
+
if (!password1 || !password2) {
|
|
17
|
+
setLocalError('Please enter the new password twice.');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (password1 !== password2) {
|
|
21
|
+
setLocalError('The passwords do not match.');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (onSubmit) {
|
|
25
|
+
onSubmit(password1);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
return (_jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { display: 'flex', flexDirection: 'column', gap: 2, mt: 2 }, children: [localError && (_jsx(Box, { sx: { colour: 'error.main', fontSize: 14 }, children: localError })), _jsx(TextField, { label: "New password", type: "password", fullWidth: true, autoComplete: "new-password", value: password1, onChange: (e) => setPassword1(e.target.value), disabled: submitting }), _jsx(TextField, { label: "Confirm new password", type: "password", fullWidth: true, autoComplete: "new-password", value: password2, onChange: (e) => setPassword2(e.target.value), disabled: submitting }), _jsx(Button, { type: "submit", variant: "contained", disabled: submitting, children: "Set password" })] }));
|
|
29
|
+
};
|
|
30
|
+
export default PasswordSetForm;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// src/components/ProfileComponent.jsx
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
import { Box, Stack, TextField, FormControlLabel, Checkbox, Button, CircularProgress, Alert, Typography, } from '@mui/material';
|
|
6
|
+
import { USERS_BASE } from '../auth/authConfig';
|
|
7
|
+
/**
|
|
8
|
+
* ProfileComponent
|
|
9
|
+
*
|
|
10
|
+
* - Lädt das aktuelle User-Objekt von `${USERS_BASE}/current/`
|
|
11
|
+
* - Zeigt Basisfelder (Name, E-Mail, etc.)
|
|
12
|
+
* - Optional Privacy/Cookie-Checkboxen
|
|
13
|
+
* - Ruft onSubmit(payload) auf, um Änderungen zu speichern
|
|
14
|
+
*/
|
|
15
|
+
export function ProfileComponent({ onLoad, onSubmit, submitText = 'Save', showName = true, showPrivacy = true, showCookies = true, }) {
|
|
16
|
+
const [loading, setLoading] = useState(true);
|
|
17
|
+
const [saving, setSaving] = useState(false);
|
|
18
|
+
const [error, setError] = useState('');
|
|
19
|
+
const [success, setSuccess] = useState('');
|
|
20
|
+
const [userId, setUserId] = useState(null);
|
|
21
|
+
const [username, setUsername] = useState('');
|
|
22
|
+
const [email, setEmail] = useState('');
|
|
23
|
+
const [firstName, setFirstName] = useState('');
|
|
24
|
+
const [lastName, setLastName] = useState('');
|
|
25
|
+
const [acceptedPrivacy, setAcceptedPrivacy] = useState(false);
|
|
26
|
+
const [acceptedCookies, setAcceptedCookies] = useState(false);
|
|
27
|
+
// Load current user on mount
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
let mounted = true;
|
|
30
|
+
const loadUser = async () => {
|
|
31
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
32
|
+
setLoading(true);
|
|
33
|
+
setError('');
|
|
34
|
+
try {
|
|
35
|
+
const res = await axios.get(`${USERS_BASE}/current/`, {
|
|
36
|
+
withCredentials: true,
|
|
37
|
+
});
|
|
38
|
+
if (!mounted)
|
|
39
|
+
return;
|
|
40
|
+
const data = res.data;
|
|
41
|
+
setUserId((_a = data.id) !== null && _a !== void 0 ? _a : null);
|
|
42
|
+
setUsername((_b = data.username) !== null && _b !== void 0 ? _b : '');
|
|
43
|
+
setEmail((_c = data.email) !== null && _c !== void 0 ? _c : '');
|
|
44
|
+
setFirstName((_d = data.first_name) !== null && _d !== void 0 ? _d : '');
|
|
45
|
+
setLastName((_e = data.last_name) !== null && _e !== void 0 ? _e : '');
|
|
46
|
+
setAcceptedPrivacy(Boolean(data.accepted_privacy_statement));
|
|
47
|
+
setAcceptedCookies(Boolean(data.accepted_convenience_cookies));
|
|
48
|
+
if (onLoad) {
|
|
49
|
+
onLoad(data);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (!mounted)
|
|
54
|
+
return;
|
|
55
|
+
setError(((_g = (_f = err.response) === null || _f === void 0 ? void 0 : _f.data) === null || _g === void 0 ? void 0 : _g.detail) ||
|
|
56
|
+
err.message ||
|
|
57
|
+
'Unable to load profile.');
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
if (mounted)
|
|
61
|
+
setLoading(false);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
loadUser();
|
|
65
|
+
return () => {
|
|
66
|
+
mounted = false;
|
|
67
|
+
};
|
|
68
|
+
}, [onLoad]);
|
|
69
|
+
const handleSubmit = async (event) => {
|
|
70
|
+
var _a, _b;
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
if (!onSubmit)
|
|
73
|
+
return;
|
|
74
|
+
setSaving(true);
|
|
75
|
+
setError('');
|
|
76
|
+
setSuccess('');
|
|
77
|
+
const payload = {
|
|
78
|
+
first_name: firstName,
|
|
79
|
+
last_name: lastName,
|
|
80
|
+
// Die Serializer-Felder sind flach, werden aber über `source="profile.*"`
|
|
81
|
+
// ins Profil gemappt.
|
|
82
|
+
accepted_privacy_statement: acceptedPrivacy,
|
|
83
|
+
accepted_convenience_cookies: acceptedCookies,
|
|
84
|
+
};
|
|
85
|
+
try {
|
|
86
|
+
await onSubmit(payload);
|
|
87
|
+
setSuccess('Profile updated successfully.');
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
setError(((_b = (_a = err.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.detail) ||
|
|
91
|
+
err.message ||
|
|
92
|
+
'Error while saving profile.');
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
setSaving(false);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
if (loading) {
|
|
99
|
+
return (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', py: 4 }, children: _jsx(CircularProgress, {}) }));
|
|
100
|
+
}
|
|
101
|
+
return (_jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { maxWidth: 600, display: 'flex', flexDirection: 'column', gap: 2 }, children: [error && (_jsx(Alert, { severity: "error", children: error })), success && (_jsx(Alert, { severity: "success", children: success })), _jsxs(Stack, { spacing: 2, children: [_jsx(TextField, { label: "Username", value: username, fullWidth: true, disabled: true }), _jsx(TextField, { label: "Email", type: "email", value: email, fullWidth: true, disabled: true })] }), showName && (_jsxs(Stack, { spacing: 2, direction: { xs: 'column', sm: 'row' }, children: [_jsx(TextField, { label: "First name", value: firstName, onChange: (e) => setFirstName(e.target.value), fullWidth: true }), _jsx(TextField, { label: "Last name", value: lastName, onChange: (e) => setLastName(e.target.value), fullWidth: true })] })), (showPrivacy || showCookies) && (_jsxs(Box, { sx: { mt: 1 }, children: [_jsx(Typography, { variant: "subtitle1", gutterBottom: true, children: "Privacy and cookies" }), _jsxs(Stack, { spacing: 1, children: [showPrivacy && (_jsx(FormControlLabel, { control: _jsx(Checkbox, { checked: acceptedPrivacy, onChange: (e) => setAcceptedPrivacy(e.target.checked) }), label: "I agree with the privacy statement." })), showCookies && (_jsx(FormControlLabel, { control: _jsx(Checkbox, { checked: acceptedCookies, onChange: (e) => setAcceptedCookies(e.target.checked) }), label: "I allow convenience cookies." }))] })] })), _jsx(Box, { sx: { mt: 2 }, children: _jsx(Button, { type: "submit", variant: "contained", disabled: saving, children: saving ? 'Saving…' : submitText }) })] }));
|
|
102
|
+
}
|
|
103
|
+
;
|
|
104
|
+
export default ProfileComponent;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import { Box, Typography, Divider, Button, Stack, Alert, } from '@mui/material';
|
|
4
|
+
import PasswordChangeForm from './PasswordChangeForm';
|
|
5
|
+
import SocialLoginButtons from './SocialLoginButtons';
|
|
6
|
+
import { authApi } from '../auth/authApi';
|
|
7
|
+
const SecurityComponent = () => {
|
|
8
|
+
const [message, setMessage] = useState('');
|
|
9
|
+
const [error, setError] = useState('');
|
|
10
|
+
const handleSocialClick = async (provider) => {
|
|
11
|
+
setMessage('');
|
|
12
|
+
setError('');
|
|
13
|
+
try {
|
|
14
|
+
// FIX: Await the async function to catch network errors
|
|
15
|
+
await authApi.startSocialLogin(provider);
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
setError(e.message || 'Social login could not be started.');
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const handlePasswordChange = async (currentPassword, newPassword) => {
|
|
22
|
+
var _a, _b;
|
|
23
|
+
setMessage('');
|
|
24
|
+
setError('');
|
|
25
|
+
try {
|
|
26
|
+
await authApi.changePassword(currentPassword, newPassword);
|
|
27
|
+
setMessage('Password changed successfully.');
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
const errorMsg = ((_b = (_a = err.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.detail) ||
|
|
31
|
+
err.message ||
|
|
32
|
+
'Could not change password.';
|
|
33
|
+
setError(errorMsg);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
return (_jsxs(Box, { children: [message && (_jsx(Alert, { severity: "success", sx: { mb: 2 }, children: message })), error && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error })), _jsx(Typography, { variant: "h6", gutterBottom: true, children: "Password" }), _jsx(PasswordChangeForm, { onSubmit: handlePasswordChange }), _jsx(Divider, { sx: { my: 3 } }), _jsx(Typography, { variant: "h6", gutterBottom: true, children: "Social logins" }), _jsx(Typography, { variant: "body2", sx: { mb: 1 }, children: "Sign in using a connected Google or Microsoft account." }), _jsx(SocialLoginButtons, { onProviderClick: handleSocialClick }), _jsx(Divider, { sx: { my: 3 } }), _jsx(Typography, { variant: "h6", gutterBottom: true, children: "Passkeys (coming soon)" }), _jsx(Typography, { variant: "body2", sx: { mb: 1 }, children: "Use passkeys for passwordless sign-in. This section will be active once WebAuthn endpoints are wired in the backend." }), _jsx(Stack, { direction: "row", spacing: 2, children: _jsx(Button, { variant: "outlined", disabled: true, children: "Add passkey" }) })] }));
|
|
37
|
+
};
|
|
38
|
+
export default SecurityComponent;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// src/auth/components/SocialLoginButtons.jsx
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Stack, Button, Box } from '@mui/material';
|
|
5
|
+
import { SOCIAL_PROVIDERS } from '../auth/authConfig';
|
|
6
|
+
/**
|
|
7
|
+
* Renders buttons for social login providers.
|
|
8
|
+
* The caller passes a handler that receives the provider key.
|
|
9
|
+
*/
|
|
10
|
+
const SocialLoginButtons = ({ onProviderClick }) => {
|
|
11
|
+
const handleClick = (provider) => {
|
|
12
|
+
if (onProviderClick) {
|
|
13
|
+
onProviderClick(provider);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
return (_jsxs(Stack, { spacing: 1.5, sx: { mt: 1 }, children: [_jsx(Button, { variant: "outlined", fullWidth: true, onClick: () => handleClick(SOCIAL_PROVIDERS.google), startIcon: _jsx(Box, { sx: {
|
|
17
|
+
width: 24,
|
|
18
|
+
height: 24,
|
|
19
|
+
borderRadius: '50%',
|
|
20
|
+
border: '1px solid rgba(0,0,0,0.2)',
|
|
21
|
+
display: 'flex',
|
|
22
|
+
alignItems: 'center',
|
|
23
|
+
justifyContent: 'center',
|
|
24
|
+
fontWeight: 700,
|
|
25
|
+
fontSize: 14,
|
|
26
|
+
}, children: "G" }), children: "Continue with Google" }), _jsx(Button, { variant: "outlined", fullWidth: true, onClick: () => handleClick(SOCIAL_PROVIDERS.microsoft), startIcon: _jsxs(Box, { sx: {
|
|
27
|
+
width: 24,
|
|
28
|
+
height: 24,
|
|
29
|
+
display: 'grid',
|
|
30
|
+
gridTemplateColumns: '1fr 1fr',
|
|
31
|
+
gridTemplateRows: '1fr 1fr',
|
|
32
|
+
gap: '1px',
|
|
33
|
+
}, children: [_jsx(Box, { sx: { bgcolor: 'primary.main', opacity: 0.9 } }), _jsx(Box, { sx: { bgcolor: 'primary.main', opacity: 0.7 } }), _jsx(Box, { sx: { bgcolor: 'primary.main', opacity: 0.7 } }), _jsx(Box, { sx: { bgcolor: 'primary.main', opacity: 0.9 } })] }), children: "Continue with Microsoft" })] }));
|
|
34
|
+
};
|
|
35
|
+
export default SocialLoginButtons;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Auth Core & Hooks
|
|
2
|
+
export { authApi } from './auth/authApi';
|
|
3
|
+
export { AuthContext, AuthProvider } from './auth/AuthContext';
|
|
4
|
+
// Falls du einen Custom Hook hast: export { useAuth } from './auth/react/useAuth';
|
|
5
|
+
// Components
|
|
6
|
+
export { NarrowPage, WidePage } from './layout/PageLayout';
|
|
7
|
+
export { LoginPage } from './pages/LoginPage';
|
|
8
|
+
export { PasswordResetRequestPage } from './pages/PasswordResetRequestPage';
|
|
9
|
+
export { PasswordChangePage } from './pages/PasswordChangePage';
|
|
10
|
+
export { PasswordInvitePage } from './pages/PasswordInvitePage';
|
|
11
|
+
export { AccountPage } from './pages/AccountPage';
|
|
12
|
+
export { ProfileComponent } from './components/ProfileComponent';
|
|
13
|
+
// Falls du noch pure UI-Komponenten hast (Formulare)
|
|
14
|
+
// export { default as LoginForm } from './components/forms/LoginForm';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// src/components/layout/PageLayout.jsx
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Container, Box, Typography } from '@mui/material';
|
|
5
|
+
// Layout for content pages with larger width (e.g. Profile, Welcome, Input)
|
|
6
|
+
export const WidePage = ({ title, children }) => (_jsxs(Container, { maxWidth: "md", sx: { mt: 4 }, children: [title && (_jsx(Typography, { variant: "h4", gutterBottom: true, children: title })), children] }));
|
|
7
|
+
// Layout for forms or narrow pages (e.g. Login, Reset, Invite)
|
|
8
|
+
export const NarrowPage = ({ title, subtitle, children }) => (_jsx(Container, { maxWidth: "md", sx: { mt: 4 }, children: _jsxs(Box, { sx: { maxWidth: 480, mx: 'auto' }, children: [title && (_jsx(Typography, { variant: "h4", gutterBottom: true, children: title })), subtitle && (_jsx(Typography, { paragraph: true, children: subtitle })), children] }) }));
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState, useContext } from 'react';
|
|
3
|
+
import { Helmet } from 'react-helmet';
|
|
4
|
+
import { Tabs, Tab, Box } from '@mui/material';
|
|
5
|
+
import { WidePage } from '../layout/PageLayout';
|
|
6
|
+
import ProfileComponent from '../components/ProfileComponent';
|
|
7
|
+
import SecurityComponent from '../components/SecurityComponent'; // Kommentiere ich ein, bis die Datei existiert, um Build-Fehler zu vermeiden
|
|
8
|
+
import { authApi } from '../auth/authApi';
|
|
9
|
+
import { AuthContext } from '../auth/AuthContext';
|
|
10
|
+
export function AccountPage() {
|
|
11
|
+
const [tab, setTab] = useState('account');
|
|
12
|
+
const { login } = useContext(AuthContext);
|
|
13
|
+
const handleTabChange = (_event, newValue) => {
|
|
14
|
+
setTab(newValue);
|
|
15
|
+
};
|
|
16
|
+
// Diese Funktion übernimmt das tatsächliche Speichern
|
|
17
|
+
const handleProfileSubmit = async (payload) => {
|
|
18
|
+
// 1. Send update to backend via PATCH
|
|
19
|
+
const updatedUser = await authApi.updateUserProfile(payload);
|
|
20
|
+
// 2. Update local global context with fresh data (so header/sidebar update immediately)
|
|
21
|
+
login(updatedUser);
|
|
22
|
+
};
|
|
23
|
+
return (_jsxs(WidePage, { title: "Account", children: [_jsx(Helmet, { children: _jsx("title", { children: "PROJECT_NAME \u2013 Account" }) }), _jsxs(Tabs, { value: tab, onChange: handleTabChange, sx: { mb: 3 }, children: [_jsx(Tab, { label: "Account", value: "account" }), _jsx(Tab, { label: "Security", value: "security" })] }), tab === 'account' && (_jsx(Box, { sx: { mt: 1 }, children: _jsx(ProfileComponent, { onLoad: () => { }, onSubmit: handleProfileSubmit, submitText: "Save", showName: true, showPrivacy: true, showCookies: true }) })), tab === 'security' && (_jsx(Box, { sx: { mt: 1 }, children: _jsx(SecurityComponent, {}) }))] }));
|
|
24
|
+
}
|
|
25
|
+
;
|
|
26
|
+
export default AccountPage;
|