@micha.bigler/ui-core-micha 1.0.0 → 1.1.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.
@@ -13,7 +13,7 @@ function extractErrorMessage(error) {
13
13
  if (Array.isArray(data.non_field_errors) && data.non_field_errors.length > 0) {
14
14
  return data.non_field_errors[0];
15
15
  }
16
- return JSON.stringify(data);
16
+ return 'An error occurred. Please try again.';
17
17
  }
18
18
  // Helper to get CSRF token from cookies manually
19
19
  function getCsrfToken() {
@@ -133,6 +133,27 @@ export async function logoutSession() {
133
133
  export function startSocialLogin(provider) {
134
134
  window.location.href = `/accounts/${provider}/login/?process=login`;
135
135
  }
136
+ /**
137
+ * Prüft, ob ein Access-Code gültig ist.
138
+ * Erwartet: POST /api/access-codes/validate/ { code }
139
+ * Antwort: { valid: true/false } oder 400 mit detail-Fehler.
140
+ */
141
+ export async function validateAccessCode(code) {
142
+ const res = await axios.post(`${ACCESS_CODES_BASE}/validate/`, { code }, { withCredentials: true });
143
+ return res.data; // { valid: bool } oder Error
144
+ }
145
+ /**
146
+ * Fordert eine Einladung mit optionalem Access-Code an.
147
+ * Backend prüft den Code noch einmal serverseitig.
148
+ */
149
+ export async function requestInviteWithCode(email, accessCode) {
150
+ const payload = { email };
151
+ if (accessCode) {
152
+ payload.access_code = accessCode;
153
+ }
154
+ const res = await axios.post(`${USERS_BASE}/invite/`, payload, { withCredentials: true });
155
+ return res.data;
156
+ }
136
157
  /**
137
158
  * Loads the current session information directly from allauth headless.
138
159
  */
@@ -169,4 +190,6 @@ export const authApi = {
169
190
  setNewPassword,
170
191
  loginWithPasskey,
171
192
  registerPasskey,
193
+ validateAccessCode,
194
+ requestInviteWithCode,
172
195
  };
@@ -9,6 +9,8 @@ export const HEADLESS_VERSION = 'v1';
9
9
  export const HEADLESS_BASE = `${AUTH_BASE}/${HEADLESS_VARIANT}/${HEADLESS_VERSION}`;
10
10
  // Eigene User-API
11
11
  export const USERS_BASE = '/api/users';
12
+ // NEU: Access-Code-API (kommt aus der Lib)
13
+ export const ACCESS_CODES_BASE = '/api/access-codes';
12
14
  // CSRF-Endpoint (Django-View csrf_token_view)
13
15
  export const CSRF_URL = '/api/csrf/';
14
16
  // Konfiguration der Social-Provider
@@ -0,0 +1,113 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // src/auth/components/AccessCodeManager.jsx
3
+ import React, { useEffect, useState } from 'react';
4
+ import axios from 'axios';
5
+ import { Box, Stack, Typography, Slider, Button, TextField, IconButton, Chip, Alert, CircularProgress, } from '@mui/material';
6
+ import CloseIcon from '@mui/icons-material/Close';
7
+ import { ACCESS_CODES_BASE } from '../auth/authConfig';
8
+ /**
9
+ * AccessCodeManager
10
+ *
11
+ * - Zeigt alle Access-Codes
12
+ * - Löschen von Codes
13
+ * - Generieren neuer Codes mit wählbarer Länge
14
+ * - Manuelles Hinzufügen eines Codes
15
+ *
16
+ * Backend-API (DRF ViewSet):
17
+ * GET /api/access-codes/ → Liste [{id, code, is_active, created_at}]
18
+ * POST /api/access-codes/ → {code: "..."} → 201
19
+ * DELETE /api/access-codes/{id}/ → 204
20
+ */
21
+ export function AccessCodeManager() {
22
+ const [codes, setCodes] = useState([]);
23
+ const [loading, setLoading] = useState(true);
24
+ const [submitting, setSubmitting] = useState(false);
25
+ const [length, setLength] = useState(12);
26
+ const [manualCode, setManualCode] = useState('');
27
+ const [error, setError] = useState('');
28
+ const [success, setSuccess] = useState('');
29
+ // Loads all access codes
30
+ const loadCodes = async () => {
31
+ var _a, _b;
32
+ setLoading(true);
33
+ setError('');
34
+ try {
35
+ const res = await axios.get(ACCESS_CODES_BASE + '/', {
36
+ withCredentials: true,
37
+ });
38
+ setCodes(res.data || []);
39
+ }
40
+ catch (err) {
41
+ 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) || (err === null || err === void 0 ? void 0 : err.message) || 'Could not load access codes.');
42
+ }
43
+ finally {
44
+ setLoading(false);
45
+ }
46
+ };
47
+ useEffect(() => {
48
+ loadCodes();
49
+ }, []);
50
+ // Generates a random code with the selected length
51
+ const generateRandomCode = (len) => {
52
+ const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
53
+ let out = '';
54
+ for (let i = 0; i < len; i += 1) {
55
+ const idx = Math.floor(Math.random() * alphabet.length);
56
+ out += alphabet[idx];
57
+ }
58
+ return out;
59
+ };
60
+ const handleCreateCode = async (code) => {
61
+ var _a, _b;
62
+ setSubmitting(true);
63
+ setError('');
64
+ setSuccess('');
65
+ try {
66
+ const res = await axios.post(ACCESS_CODES_BASE + '/', { code }, { withCredentials: true });
67
+ setCodes((prev) => [...prev, res.data]);
68
+ setSuccess('Access code saved.');
69
+ }
70
+ catch (err) {
71
+ 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) ||
72
+ (err === null || err === void 0 ? void 0 : err.message) ||
73
+ 'Could not save access code.');
74
+ }
75
+ finally {
76
+ setSubmitting(false);
77
+ }
78
+ };
79
+ const handleGenerateClick = async () => {
80
+ const code = generateRandomCode(length);
81
+ await handleCreateCode(code);
82
+ };
83
+ const handleAddManual = async () => {
84
+ if (!manualCode.trim()) {
85
+ setError('Please enter a code.');
86
+ return;
87
+ }
88
+ await handleCreateCode(manualCode.trim());
89
+ setManualCode('');
90
+ };
91
+ const handleDelete = async (id) => {
92
+ var _a, _b;
93
+ setError('');
94
+ setSuccess('');
95
+ try {
96
+ await axios.delete(`${ACCESS_CODES_BASE}/${id}/`, {
97
+ withCredentials: true,
98
+ });
99
+ setCodes((prev) => prev.filter((c) => c.id !== id));
100
+ setSuccess('Access code deleted.');
101
+ }
102
+ catch (err) {
103
+ 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) ||
104
+ (err === null || err === void 0 ? void 0 : err.message) ||
105
+ 'Could not delete access code.');
106
+ }
107
+ };
108
+ if (loading) {
109
+ return (_jsx(Box, { sx: { py: 3, display: 'flex', justifyContent: 'center' }, children: _jsx(CircularProgress, {}) }));
110
+ }
111
+ return (_jsxs(Box, { sx: { mt: 2 }, children: [error && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error })), success && (_jsx(Alert, { severity: "success", sx: { mb: 2 }, children: success })), _jsxs(Box, { sx: { mb: 3 }, children: [_jsx(Typography, { variant: "subtitle1", gutterBottom: true, children: "Active access codes" }), codes.length === 0 ? (_jsx(Typography, { variant: "body2", children: "No access codes defined." })) : (_jsx(Stack, { direction: "row", flexWrap: "wrap", gap: 1, children: codes.map((code) => (_jsx(Chip, { label: code.code, onDelete: () => handleDelete(code.id), deleteIcon: _jsx(CloseIcon, {}) }, code.id))) }))] }), _jsxs(Box, { sx: { mb: 3 }, children: [_jsx(Typography, { variant: "subtitle1", gutterBottom: true, children: "Generate new code" }), _jsxs(Box, { sx: { maxWidth: 360 }, children: [_jsxs(Typography, { variant: "body2", gutterBottom: true, children: ["Length: ", length] }), _jsx(Slider, { min: 6, max: 32, step: 1, value: length, onChange: (_, val) => setLength(val), valueLabelDisplay: "auto", disabled: submitting })] }), _jsx(Button, { variant: "contained", sx: { mt: 1 }, onClick: handleGenerateClick, disabled: submitting, children: submitting ? 'Saving…' : 'Generate code' })] }), _jsxs(Box, { sx: { mb: 2, maxWidth: 360 }, children: [_jsx(Typography, { variant: "subtitle1", gutterBottom: true, children: "Add code manually" }), _jsxs(Box, { sx: { display: 'flex', gap: 1 }, children: [_jsx(TextField, { label: "Access code", fullWidth: true, value: manualCode, onChange: (e) => setManualCode(e.target.value), disabled: submitting }), _jsx(Button, { variant: "outlined", onClick: handleAddManual, disabled: submitting, children: "Add" })] })] })] }));
112
+ }
113
+ export default AccessCodeManager;
@@ -3,7 +3,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import React, { useState } from 'react';
4
4
  import { Box, TextField, Button, Typography, Divider, } from '@mui/material';
5
5
  import SocialLoginButtons from './SocialLoginButtons';
6
- const LoginForm = ({ onSubmit, onForgotPassword, onSocialLogin, error, disabled = false, }) => {
6
+ const LoginForm = ({ onSubmit, onForgotPassword, onSocialLogin, onSignUp, // <--- NEU
7
+ error, disabled = false, }) => {
7
8
  const [identifier, setIdentifier] = useState('');
8
9
  const [password, setPassword] = useState('');
9
10
  const handleSubmit = (event) => {
@@ -17,6 +18,7 @@ const LoginForm = ({ onSubmit, onForgotPassword, onSocialLogin, error, disabled
17
18
  justifyContent: 'space-between',
18
19
  alignItems: 'center',
19
20
  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
+ gap: 1,
22
+ }, children: [_jsxs(Box, { sx: { display: 'flex', gap: 1 }, children: [_jsx(Button, { type: "submit", variant: "contained", disabled: disabled, children: "Login" }), onSignUp && (_jsx(Button, { type: "button", variant: "outlined", onClick: onSignUp, disabled: disabled, children: "Sign up" }))] }), _jsx(Button, { type: "button", variant: "outlined", onClick: onForgotPassword, disabled: disabled, children: "Forgot password?" })] }), _jsx(Divider, { sx: { my: 2 }, children: "or" }), _jsx(SocialLoginButtons, { onProviderClick: onSocialLogin })] }));
21
23
  };
22
24
  export default LoginForm;
package/dist/index.js CHANGED
@@ -10,5 +10,7 @@ export { PasswordChangePage } from './pages/PasswordChangePage';
10
10
  export { PasswordInvitePage } from './pages/PasswordInvitePage';
11
11
  export { AccountPage } from './pages/AccountPage';
12
12
  export { ProfileComponent } from './components/ProfileComponent';
13
+ export { SignUpPage } from './pages/SignUpPage';
14
+ export { AccessCodeManager } from './components/AccessCodeManager';
13
15
  // Falls du noch pure UI-Komponenten hast (Formulare)
14
16
  // export { default as LoginForm } from './components/forms/LoginForm';
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // src/pages/LoginPage.jsx (oder wo deine LoginPage liegt)
2
3
  import React, { useState, useContext } from 'react';
3
4
  import { useNavigate } from 'react-router-dom';
4
5
  import { Helmet } from 'react-helmet';
@@ -39,12 +40,14 @@ export function LoginPage() {
39
40
  authApi.startSocialLogin(providerKey);
40
41
  }
41
42
  catch (err) {
42
- // This will only fire on immediate client-side errors
43
+ // eslint-disable-next-line no-console
43
44
  console.error('Social login init failed', err);
44
45
  setError('Could not start social login.');
45
46
  }
46
47
  };
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
+ const handleSignUp = () => {
49
+ navigate('/signup');
50
+ };
51
+ 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, onSignUp: handleSignUp, error: undefined, disabled: submitting })] }));
48
52
  }
49
- ;
50
53
  export default LoginPage;
@@ -29,7 +29,7 @@ export function PasswordResetRequestPage() {
29
29
  setSubmitting(false);
30
30
  }
31
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 })] }));
32
+ return (_jsxs(NarrowPage, { title: "Reset password", children: [_jsx(Helmet, { children: _jsx("title", { children: "PROJECT_NAME \u2013 Reset password" }) }), successMessage && (_jsx(Typography, { color: "primary", gutterBottom: true, children: successMessage })), error && (_jsx(Typography, { color: "error", gutterBottom: true, children: error })), _jsx(PasswordResetRequestForm, { onSubmit: handleSubmit, submitting: submitting })] }));
33
33
  }
34
34
  ;
35
35
  export default PasswordResetRequestPage;
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // src/pages/SignUpPage.jsx
3
+ import React, { useState } from 'react';
4
+ import { useNavigate, Link as RouterLink } from 'react-router-dom';
5
+ import { Box, TextField, Button, Typography, Alert, } from '@mui/material';
6
+ import { Helmet } from 'react-helmet';
7
+ import { NarrowPage } from '../layout/PageLayout';
8
+ import { authApi } from '../auth/authApi';
9
+ export function SignUpPage() {
10
+ const navigate = useNavigate();
11
+ const [email, setEmail] = useState('');
12
+ const [accessCode, setAccessCode] = useState('');
13
+ const [submitting, setSubmitting] = useState(false);
14
+ const [success, setSuccess] = useState('');
15
+ const [error, setError] = useState('');
16
+ const handleSubmit = async (event) => {
17
+ var _a, _b;
18
+ event.preventDefault();
19
+ setSuccess('');
20
+ setError('');
21
+ if (!email) {
22
+ setError('Please enter an email address.');
23
+ return;
24
+ }
25
+ if (!accessCode) {
26
+ setError('Please enter an access code.');
27
+ return;
28
+ }
29
+ setSubmitting(true);
30
+ try {
31
+ // 1) Access-Code prüfen (API /api/access-codes/validate/)
32
+ const res = await authApi.validateAccessCode(accessCode);
33
+ if (!res.valid) {
34
+ setError('Access code is invalid.');
35
+ return;
36
+ }
37
+ // 2) Invite anfordern (API /api/users/invite/)
38
+ await authApi.requestInviteWithCode(email, accessCode);
39
+ setSuccess(`If the access code is valid for this project, an invitation link has been sent to ${email}.`);
40
+ }
41
+ catch (err) {
42
+ const detail = ((_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) ||
43
+ (err === null || err === void 0 ? void 0 : err.message) ||
44
+ 'Could not request invitation.';
45
+ setError(detail);
46
+ }
47
+ finally {
48
+ setSubmitting(false);
49
+ }
50
+ };
51
+ const handleGoToLogin = () => {
52
+ navigate('/login');
53
+ };
54
+ return (_jsxs(NarrowPage, { title: "Sign up", children: [_jsx(Helmet, { children: _jsx("title", { children: "PROJECT_NAME \u2013 Sign up" }) }), success && (_jsx(Alert, { severity: "success", sx: { mb: 2 }, children: success })), error && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error })), _jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { display: 'flex', flexDirection: 'column', gap: 2 }, children: [_jsx(TextField, { label: "Email address", type: "email", required: true, fullWidth: true, value: email, onChange: (e) => setEmail(e.target.value), disabled: submitting }), _jsx(TextField, { label: "Access code", type: "text", required: true, fullWidth: true, value: accessCode, onChange: (e) => setAccessCode(e.target.value), disabled: submitting }), _jsx(Button, { type: "submit", variant: "contained", disabled: submitting, children: submitting ? 'Submitting…' : 'Request invitation' })] }), _jsx(Box, { sx: { mt: 3 }, children: _jsxs(Typography, { variant: "body2", children: ["Already have an account?", ' ', _jsx(Button, { onClick: handleGoToLogin, variant: "text", size: "small", children: "Go to login" })] }) })] }));
55
+ }
56
+ export default SignUpPage;
package/package.json CHANGED
@@ -1,23 +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
- }
1
+ {
2
+ "name": "@micha.bigler/ui-core-micha",
3
+ "version": "1.1.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": "^7.3.5",
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
+ }
@@ -1,108 +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
- };
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
+ };