@micha.bigler/ui-core-micha 2.1.16 → 2.1.18

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.
@@ -2,33 +2,50 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  // src/auth/AuthContext.jsx
3
3
  import React, { createContext, useState, useEffect, } from 'react';
4
4
  import { ensureCsrfToken } from './apiClient'; // <--- IMPORT ADDED
5
- import { fetchCurrentUser, logoutSession, } from './authApi';
5
+ import { fetchAuthMethods, fetchCurrentUser, logoutSession, } from './authApi';
6
6
  export const AuthContext = createContext(null);
7
+ const DEFAULT_AUTH_METHODS = {
8
+ password_login: true,
9
+ password_reset: true,
10
+ signup: true,
11
+ password_change: true,
12
+ social_login: true,
13
+ social_providers: ['google', 'microsoft'],
14
+ passkey_login: true,
15
+ passkeys_manage: true,
16
+ mfa_totp: true,
17
+ mfa_recovery_codes: true,
18
+ mfa_enabled: true,
19
+ };
7
20
  export const AuthProvider = ({ children }) => {
8
21
  const [user, setUser] = useState(null);
22
+ const [authMethods, setAuthMethods] = useState(DEFAULT_AUTH_METHODS);
9
23
  const [loading, setLoading] = useState(true);
24
+ const mapUserFromApi = (data) => {
25
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
26
+ const profile = (data === null || data === void 0 ? void 0 : data.profile) || {};
27
+ return Object.assign(Object.assign({}, data), { id: data === null || data === void 0 ? void 0 : data.id, username: data === null || data === void 0 ? void 0 : data.username, email: data === null || data === void 0 ? void 0 : data.email, first_name: data === null || data === void 0 ? void 0 : data.first_name, last_name: data === null || data === void 0 ? void 0 : data.last_name, role: (_b = (_a = data === null || data === void 0 ? void 0 : data.role) !== null && _a !== void 0 ? _a : profile === null || profile === void 0 ? void 0 : profile.role) !== null && _b !== void 0 ? _b : null, language: (_d = (_c = data === null || data === void 0 ? void 0 : data.language) !== null && _c !== void 0 ? _c : profile === null || profile === void 0 ? void 0 : profile.language) !== null && _d !== void 0 ? _d : 'en', is_superuser: Boolean(data === null || data === void 0 ? void 0 : data.is_superuser), is_new: Boolean((_e = data === null || data === void 0 ? void 0 : data.is_new) !== null && _e !== void 0 ? _e : profile === null || profile === void 0 ? void 0 : profile.is_new), is_invited: Boolean((_f = data === null || data === void 0 ? void 0 : data.is_invited) !== null && _f !== void 0 ? _f : profile === null || profile === void 0 ? void 0 : profile.is_invited), accepted_privacy_statement: Boolean((_g = data === null || data === void 0 ? void 0 : data.accepted_privacy_statement) !== null && _g !== void 0 ? _g : profile === null || profile === void 0 ? void 0 : profile.accepted_privacy_statement), accepted_convenience_cookies: Boolean((_h = data === null || data === void 0 ? void 0 : data.accepted_convenience_cookies) !== null && _h !== void 0 ? _h : profile === null || profile === void 0 ? void 0 : profile.accepted_convenience_cookies), is_support_agent: Boolean((_j = data === null || data === void 0 ? void 0 : data.is_support_agent) !== null && _j !== void 0 ? _j : profile === null || profile === void 0 ? void 0 : profile.is_support_agent), support_contact_id: (_l = (_k = data === null || data === void 0 ? void 0 : data.support_contact_id) !== null && _k !== void 0 ? _k : profile === null || profile === void 0 ? void 0 : profile.support_contact_id) !== null && _l !== void 0 ? _l : null, security_state: data === null || data === void 0 ? void 0 : data.security_state, available_roles: (data === null || data === void 0 ? void 0 : data.available_roles) || [], ui_permissions: (data === null || data === void 0 ? void 0 : data.ui_permissions) || {}, can_manage_support_agents: Boolean(data === null || data === void 0 ? void 0 : data.can_manage_support_agents), can_manage: Boolean(data === null || data === void 0 ? void 0 : data.can_manage), is_active: data === null || data === void 0 ? void 0 : data.is_active, last_login: data === null || data === void 0 ? void 0 : data.last_login, date_joined: data === null || data === void 0 ? void 0 : data.date_joined });
28
+ };
10
29
  useEffect(() => {
11
30
  let isMounted = true;
12
31
  const initAuth = async () => {
13
32
  try {
14
33
  // 1) Ensure CSRF cookie exists using the specific client
15
34
  await ensureCsrfToken();
16
- // 2) Load user
35
+ // 2) Load auth methods (public)
36
+ try {
37
+ const methods = await fetchAuthMethods();
38
+ if (isMounted && methods && typeof methods === 'object') {
39
+ setAuthMethods((prev) => (Object.assign(Object.assign({}, prev), methods)));
40
+ }
41
+ }
42
+ catch (_a) {
43
+ // Keep defaults; login UI remains usable.
44
+ }
45
+ // 3) Load user
17
46
  const data = await fetchCurrentUser();
18
47
  if (isMounted) {
19
- // Map data to ensure consistent structure
20
- setUser({
21
- id: data.id,
22
- username: data.username,
23
- email: data.email,
24
- first_name: data.first_name,
25
- last_name: data.last_name,
26
- role: data.role,
27
- is_superuser: data.is_superuser,
28
- security_state: data.security_state,
29
- available_roles: data.available_roles,
30
- ui_permissions: data.ui_permissions,
31
- });
48
+ setUser(mapUserFromApi(data));
32
49
  }
33
50
  }
34
51
  catch (err) {
@@ -45,7 +62,7 @@ export const AuthProvider = ({ children }) => {
45
62
  return () => { isMounted = false; };
46
63
  }, []);
47
64
  const login = (userData) => {
48
- setUser((prev) => (Object.assign(Object.assign({}, prev), userData)));
65
+ setUser((prev) => (Object.assign(Object.assign({}, prev), mapUserFromApi(userData))));
49
66
  };
50
67
  const logout = async () => {
51
68
  try {
@@ -61,6 +78,7 @@ export const AuthProvider = ({ children }) => {
61
78
  };
62
79
  return (_jsx(AuthContext.Provider, { value: {
63
80
  user,
81
+ authMethods,
64
82
  loading,
65
83
  login,
66
84
  logout,
@@ -15,6 +15,10 @@ export async function fetchCurrentUser() {
15
15
  const res = await apiClient.get(`${USERS_BASE}/current/`);
16
16
  return res.data;
17
17
  }
18
+ export async function fetchAuthMethods() {
19
+ const res = await apiClient.get('/api/auth-methods/');
20
+ return res.data || {};
21
+ }
18
22
  export async function updateUserProfile(data) {
19
23
  try {
20
24
  const res = await apiClient.patch(`${USERS_BASE}/current/`, data);
@@ -24,6 +28,21 @@ export async function updateUserProfile(data) {
24
28
  throw normaliseApiError(error, 'Auth.PROFILE_UPDATE_FAILED');
25
29
  }
26
30
  }
31
+ function normalizeStatementText(data) {
32
+ if (typeof data === 'string')
33
+ return data;
34
+ if (data && typeof data.content === 'string')
35
+ return data.content;
36
+ return '';
37
+ }
38
+ export async function fetchPrivacyStatement() {
39
+ const res = await apiClient.get('/api/utils/privacy/');
40
+ return normalizeStatementText(res.data);
41
+ }
42
+ export async function fetchCookieStatement() {
43
+ const res = await apiClient.get('/api/utils/cookie/');
44
+ return normalizeStatementText(res.data);
45
+ }
27
46
  export async function fetchHeadlessSession() {
28
47
  const res = await apiClient.get(`${HEADLESS_BASE}/auth/session`);
29
48
  return res.data;
@@ -8,6 +8,12 @@ import { useTranslation } from 'react-i18next';
8
8
  import { fetchAccessCodes, createAccessCode, deleteAccessCode, } from '../auth/authApi';
9
9
  export function AccessCodeManager() {
10
10
  const { t } = useTranslation();
11
+ const actionButtonSx = {
12
+ minWidth: 120,
13
+ height: 40,
14
+ textTransform: 'none',
15
+ whiteSpace: 'nowrap',
16
+ };
11
17
  const [codes, setCodes] = useState([]);
12
18
  const [loading, setLoading] = useState(true);
13
19
  const [submitting, setSubmitting] = useState(false);
@@ -90,7 +96,7 @@ export function AccessCodeManager() {
90
96
  if (loading) {
91
97
  return (_jsx(Box, { sx: { py: 3, display: 'flex', justifyContent: 'center' }, children: _jsx(CircularProgress, {}) }));
92
98
  }
93
- return (_jsxs(Box, { sx: { mt: 2 }, children: [errorKey && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(errorKey) })), successKey && (_jsx(Alert, { severity: "success", sx: { mb: 2 }, children: t(successKey) })), _jsxs(Box, { sx: { mb: 3 }, children: [_jsx(Typography, { variant: "subtitle1", gutterBottom: true, children: t('Auth.ACCESS_CODE_SECTION_ACTIVE') }), codes.length === 0 ? (_jsx(Typography, { variant: "body2", children: t('Auth.ACCESS_CODE_NONE') })) : (_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: t('Auth.ACCESS_CODE_SECTION_GENERATE') }), _jsxs(Box, { sx: { maxWidth: 360 }, children: [_jsx(Typography, { variant: "body2", gutterBottom: true, children: t('Auth.ACCESS_CODE_LENGTH_LABEL', { 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
99
+ return (_jsxs(Box, { children: [errorKey && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(errorKey) })), successKey && (_jsx(Alert, { severity: "success", sx: { mb: 2 }, children: t(successKey) })), _jsxs(Box, { sx: { mb: 3 }, children: [_jsx(Typography, { variant: "subtitle1", gutterBottom: true, children: t('Auth.ACCESS_CODE_SECTION_ACTIVE') }), codes.length === 0 ? (_jsx(Typography, { variant: "body2", children: t('Auth.ACCESS_CODE_NONE') })) : (_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: t('Auth.ACCESS_CODE_SECTION_GENERATE') }), _jsxs(Box, { sx: { maxWidth: 360 }, children: [_jsx(Typography, { variant: "body2", gutterBottom: true, children: t('Auth.ACCESS_CODE_LENGTH_LABEL', { length }) }), _jsx(Slider, { min: 6, max: 32, step: 1, value: length, onChange: (_, val) => setLength(val), valueLabelDisplay: "auto", disabled: submitting })] }), _jsx(Button, { variant: "contained", size: "small", sx: Object.assign(Object.assign({}, actionButtonSx), { mt: 1 }), onClick: handleGenerateClick, disabled: submitting, children: submitting
94
100
  ? t('Auth.SAVE_BUTTON_LOADING')
95
- : t('Auth.ACCESS_CODE_GENERATE_BUTTON') })] }), _jsxs(Box, { sx: { mb: 2, maxWidth: 360 }, children: [_jsx(Typography, { variant: "subtitle1", gutterBottom: true, children: t('Auth.ACCESS_CODE_SECTION_MANUAL') }), _jsxs(Box, { sx: { display: 'flex', gap: 1 }, children: [_jsx(TextField, { label: t('Auth.ACCESS_CODE_LABEL'), fullWidth: true, value: manualCode, onChange: (e) => setManualCode(e.target.value), disabled: submitting }), _jsx(Button, { variant: "outlined", onClick: handleAddManual, disabled: submitting, children: t('Auth.ACCESS_CODE_ADD_BUTTON') })] })] })] }));
101
+ : t('Auth.ACCESS_CODE_GENERATE_BUTTON') })] }), _jsxs(Box, { sx: { mb: 2, maxWidth: 360 }, children: [_jsx(Typography, { variant: "subtitle1", gutterBottom: true, children: t('Auth.ACCESS_CODE_SECTION_MANUAL') }), _jsxs(Box, { sx: { display: 'flex', gap: 1, alignItems: 'center' }, children: [_jsx(TextField, { label: t('Auth.ACCESS_CODE_LABEL'), fullWidth: true, size: "small", value: manualCode, onChange: (e) => setManualCode(e.target.value), disabled: submitting }), _jsx(Button, { variant: "contained", size: "small", onClick: handleAddManual, disabled: submitting, sx: actionButtonSx, children: t('Auth.ACCESS_CODE_ADD_BUTTON') })] })] })] }));
96
102
  }
@@ -26,6 +26,12 @@ function parseEmailsFromCsv(text) {
26
26
  }
27
27
  export function BulkInviteCsvTab({ inviteFn = (email) => requestInviteWithCode(email, null), onCompleted, }) {
28
28
  const { t } = useTranslation();
29
+ const actionButtonSx = {
30
+ minWidth: 120,
31
+ height: 40,
32
+ textTransform: 'none',
33
+ whiteSpace: 'nowrap',
34
+ };
29
35
  const [emails, setEmails] = useState([]);
30
36
  const [results, setResults] = useState({});
31
37
  const [busy, setBusy] = useState(false);
@@ -89,7 +95,7 @@ export function BulkInviteCsvTab({ inviteFn = (email) => requestInviteWithCode(e
89
95
  if (onCompleted)
90
96
  onCompleted(nextResults);
91
97
  };
92
- return (_jsxs(Box, { sx: { mt: 1 }, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Account.BULK_INVITE_TITLE', 'Bulk Invite via CSV') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Account.BULK_INVITE_HINT', 'Upload a CSV file containing email addresses. Header "email" is supported.') }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error }), success && _jsx(Alert, { severity: "success", sx: { mb: 2 }, children: success }), _jsxs(Box, { sx: { display: 'flex', gap: 2, alignItems: 'center', mb: 2, flexWrap: 'wrap' }, children: [_jsxs(Button, { variant: "outlined", component: "label", disabled: busy, children: [t('Account.BULK_INVITE_UPLOAD', 'Upload CSV'), _jsx("input", { type: "file", accept: ".csv,text/csv", hidden: true, onChange: handleFile })] }), _jsx(Button, { variant: "contained", onClick: handleInviteAll, disabled: busy || emails.length === 0, children: t('Account.BULK_INVITE_SEND', 'Send Invites') }), _jsx(Typography, { variant: "body2", children: t('Account.BULK_INVITE_COUNT', '{{count}} emails loaded', { count: emails.length }) })] }), busy && (_jsx(Box, { sx: { mb: 2 }, children: _jsx(LinearProgress, { variant: "determinate", value: progress }) })), emails.length > 0 && (_jsx(TableContainer, { component: Paper, children: _jsxs(Table, { size: "small", children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { children: t('Auth.EMAIL_LABEL', 'Email') }), _jsx(TableCell, { children: t('Common.STATUS', 'Status') }), _jsx(TableCell, { children: t('Common.DETAILS', 'Details') })] }) }), _jsx(TableBody, { children: emails.map((email) => {
98
+ return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Account.BULK_INVITE_TITLE', 'Bulk Invite via CSV') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Account.BULK_INVITE_HINT', 'Upload a CSV file containing email addresses. Header "email" is supported.') }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error }), success && _jsx(Alert, { severity: "success", sx: { mb: 2 }, children: success }), _jsxs(Box, { sx: { display: 'flex', gap: 2, alignItems: 'center', mb: 2, flexWrap: 'wrap' }, children: [_jsxs(Button, { variant: "outlined", size: "small", component: "label", disabled: busy, sx: actionButtonSx, children: [t('Account.BULK_INVITE_UPLOAD', 'Upload CSV'), _jsx("input", { type: "file", accept: ".csv,text/csv", hidden: true, onChange: handleFile })] }), _jsx(Button, { variant: "contained", size: "small", sx: actionButtonSx, onClick: handleInviteAll, disabled: busy || emails.length === 0, children: t('Account.BULK_INVITE_SEND', 'Send Invites') }), _jsx(Typography, { variant: "body2", children: t('Account.BULK_INVITE_COUNT', '{{count}} emails loaded', { count: emails.length }) })] }), busy && (_jsx(Box, { sx: { mb: 2 }, children: _jsx(LinearProgress, { variant: "determinate", value: progress }) })), emails.length > 0 && (_jsx(TableContainer, { component: Paper, children: _jsxs(Table, { size: "small", children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { children: t('Auth.EMAIL_LABEL', 'Email') }), _jsx(TableCell, { children: t('Common.STATUS', 'Status') }), _jsx(TableCell, { children: t('Common.DETAILS', 'Details') })] }) }), _jsx(TableBody, { children: emails.map((email) => {
93
99
  const row = results[email];
94
100
  return (_jsxs(TableRow, { children: [_jsx(TableCell, { children: email }), _jsx(TableCell, { children: row ? (row.ok ? t('Common.SUCCESS', 'Success') : t('Common.ERROR', 'Error')) : '-' }), _jsx(TableCell, { children: (row === null || row === void 0 ? void 0 : row.message) || '-' })] }, email));
95
101
  }) })] }) })), done > 0 && (_jsxs(Typography, { variant: "body2", sx: { mt: 2 }, children: [t('Account.BULK_INVITE_PROGRESS', '{{done}} / {{total}} processed', { done, total }), ' - ', t('Account.BULK_INVITE_SUCCESS_COUNT', '{{count}} successful', { count: successCount })] }))] }));
@@ -3,7 +3,7 @@ import React, { useState, useEffect } from 'react';
3
3
  import { Box, TextField, Button, Typography, Divider, } from '@mui/material';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { SocialLoginButtons } from './SocialLoginButtons';
6
- export function LoginForm({ onSubmit, onForgotPassword, onSocialLogin, onPasskeyLogin, onSignUp, error, // bereits übersetzter Text oder t(errorKey) aus dem Parent
6
+ export function LoginForm({ onSubmit, onForgotPassword, onSocialLogin, socialProviders, onPasskeyLogin, onSignUp, error, // bereits übersetzter Text oder t(errorKey) aus dem Parent
7
7
  disabled = false, initialIdentifier = '', }) {
8
8
  const { t } = useTranslation();
9
9
  const [identifier, setIdentifier] = useState(initialIdentifier);
@@ -21,6 +21,6 @@ disabled = false, initialIdentifier = '', }) {
21
21
  const supportsPasskey = !!onPasskeyLogin &&
22
22
  typeof window !== 'undefined' &&
23
23
  !!window.PublicKeyCredential;
24
- return (_jsxs(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 3 }, children: [error && (_jsx(Typography, { color: "error", gutterBottom: true, children: error })), supportsPasskey && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "contained", fullWidth: true, type: "button", onClick: onPasskeyLogin, disabled: disabled, children: t('Auth.LOGIN_USE_PASSKEY_BUTTON') }), _jsx(Divider, { sx: { my: 2 }, children: t('Auth.LOGIN_OR') })] })), _jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { display: 'flex', flexDirection: 'column', gap: 2 }, children: [_jsx(TextField, { label: t('Auth.EMAIL_LABEL'), type: "email", required: true, fullWidth: true, value: identifier, onChange: (e) => setIdentifier(e.target.value), disabled: disabled }), _jsx(TextField, { label: t('Auth.LOGIN_PASSWORD_LABEL'), type: "password", required: true, fullWidth: true, value: password, onChange: (e) => setPassword(e.target.value), disabled: disabled }), _jsx(Button, { type: "submit", variant: "contained", fullWidth: true, disabled: disabled, children: t('Auth.PAGE_LOGIN_TITLE') })] }), _jsxs(Box, { children: [_jsx(Typography, { variant: "subtitle2", sx: { mb: 1 }, children: t('Auth.LOGIN_ACCOUNT_RECOVERY_TITLE') }), _jsxs(Box, { sx: { display: 'flex', gap: 1, flexWrap: 'wrap' }, children: [onSignUp && (_jsx(Button, { type: "button", variant: "outlined", onClick: onSignUp, disabled: disabled, children: t('Auth.LOGIN_SIGNUP_BUTTON') })), _jsx(Button, { type: "button", variant: "outlined", onClick: onForgotPassword, disabled: disabled, children: t('Auth.LOGIN_FORGOT_PASSWORD_BUTTON') })] })] })] }));
24
+ return (_jsxs(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 3 }, children: [error && (_jsx(Typography, { color: "error", gutterBottom: true, children: error })), supportsPasskey && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "contained", fullWidth: true, type: "button", onClick: onPasskeyLogin, disabled: disabled, children: t('Auth.LOGIN_USE_PASSKEY_BUTTON') }), _jsx(Divider, { sx: { my: 2 }, children: t('Auth.LOGIN_OR') })] })), onSubmit && (_jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { display: 'flex', flexDirection: 'column', gap: 2 }, children: [_jsx(TextField, { label: t('Auth.EMAIL_LABEL'), type: "email", required: true, fullWidth: true, value: identifier, onChange: (e) => setIdentifier(e.target.value), disabled: disabled }), _jsx(TextField, { label: t('Auth.LOGIN_PASSWORD_LABEL'), type: "password", required: true, fullWidth: true, value: password, onChange: (e) => setPassword(e.target.value), disabled: disabled }), _jsx(Button, { type: "submit", variant: "contained", fullWidth: true, disabled: disabled, children: t('Auth.PAGE_LOGIN_TITLE') })] })), onSocialLogin && (_jsxs(Box, { children: [_jsx(Divider, { sx: { my: 2 }, children: t('Auth.LOGIN_OR') }), _jsx(SocialLoginButtons, { onProviderClick: onSocialLogin, providers: socialProviders })] })), (onSignUp || onForgotPassword) && (_jsxs(Box, { children: [_jsx(Typography, { variant: "subtitle2", sx: { mb: 1 }, children: t('Auth.LOGIN_ACCOUNT_RECOVERY_TITLE') }), _jsxs(Box, { sx: { display: 'flex', gap: 1, flexWrap: 'wrap' }, children: [onSignUp && (_jsx(Button, { type: "button", variant: "outlined", onClick: onSignUp, disabled: disabled, children: t('Auth.LOGIN_SIGNUP_BUTTON') })), onForgotPassword && (_jsx(Button, { type: "button", variant: "outlined", onClick: onForgotPassword, disabled: disabled, children: t('Auth.LOGIN_FORGOT_PASSWORD_BUTTON') }))] })] }))] }));
25
25
  }
26
26
  ;
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // src/components/ProfileComponent.jsx
3
3
  import React, { useEffect, useState, useContext } from 'react';
4
- import { Box, Stack, TextField, FormControlLabel, Checkbox, Button, CircularProgress, Alert, Typography, } from '@mui/material';
4
+ import { Box, Stack, TextField, FormControlLabel, Checkbox, Button, CircularProgress, Alert, Typography, Accordion, AccordionSummary, AccordionDetails, } from '@mui/material';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  // WICHTIG: Importiere den Context, um die bereits geladenen Daten zu nutzen
7
7
  import { AuthContext } from '../auth/AuthContext';
8
- export function ProfileComponent({ onSubmit, submitText, showName = true, showPrivacy = true, showCookies = true, }) {
8
+ import { fetchCookieStatement, fetchPrivacyStatement } from '../auth/authApi';
9
+ export function ProfileComponent({ onSubmit, submitText, showName = true, showPrivacy = true, showCookies = true, showStatements = true, privacyStatementText = null, cookieStatementText = null, }) {
9
10
  const { t } = useTranslation();
10
11
  // WICHTIG: Wir holen den User direkt aus dem globalen State
11
12
  // Das verhindert den doppelten Request und den ReferenceError
@@ -20,6 +21,10 @@ export function ProfileComponent({ onSubmit, submitText, showName = true, showPr
20
21
  const [lastName, setLastName] = useState('');
21
22
  const [acceptedPrivacy, setAcceptedPrivacy] = useState(false);
22
23
  const [acceptedCookies, setAcceptedCookies] = useState(false);
24
+ const [privacyStatement, setPrivacyStatement] = useState(privacyStatementText || '');
25
+ const [cookieStatement, setCookieStatement] = useState(cookieStatementText || '');
26
+ const [loadingPrivacyStatement, setLoadingPrivacyStatement] = useState(false);
27
+ const [loadingCookieStatement, setLoadingCookieStatement] = useState(false);
23
28
  // Synchronisiere Formular-Daten, sobald der User aus dem Context da ist
24
29
  useEffect(() => {
25
30
  var _a, _b, _c, _d;
@@ -32,6 +37,59 @@ export function ProfileComponent({ onSubmit, submitText, showName = true, showPr
32
37
  setAcceptedCookies(Boolean(user.accepted_convenience_cookies));
33
38
  }
34
39
  }, [user]);
40
+ useEffect(() => {
41
+ if (privacyStatementText !== null && privacyStatementText !== undefined) {
42
+ setPrivacyStatement(String(privacyStatementText));
43
+ }
44
+ }, [privacyStatementText]);
45
+ useEffect(() => {
46
+ if (cookieStatementText !== null && cookieStatementText !== undefined) {
47
+ setCookieStatement(String(cookieStatementText));
48
+ }
49
+ }, [cookieStatementText]);
50
+ useEffect(() => {
51
+ let cancelled = false;
52
+ const loadStatements = async () => {
53
+ if (!showStatements)
54
+ return;
55
+ if (showPrivacy && (privacyStatementText === null || privacyStatementText === undefined)) {
56
+ setLoadingPrivacyStatement(true);
57
+ try {
58
+ const text = await fetchPrivacyStatement();
59
+ if (!cancelled)
60
+ setPrivacyStatement(text || '');
61
+ }
62
+ catch (err) {
63
+ if (!cancelled)
64
+ setPrivacyStatement('');
65
+ }
66
+ finally {
67
+ if (!cancelled)
68
+ setLoadingPrivacyStatement(false);
69
+ }
70
+ }
71
+ if (showCookies && (cookieStatementText === null || cookieStatementText === undefined)) {
72
+ setLoadingCookieStatement(true);
73
+ try {
74
+ const text = await fetchCookieStatement();
75
+ if (!cancelled)
76
+ setCookieStatement(text || '');
77
+ }
78
+ catch (err) {
79
+ if (!cancelled)
80
+ setCookieStatement('');
81
+ }
82
+ finally {
83
+ if (!cancelled)
84
+ setLoadingCookieStatement(false);
85
+ }
86
+ }
87
+ };
88
+ loadStatements();
89
+ return () => {
90
+ cancelled = true;
91
+ };
92
+ }, [showStatements, showPrivacy, showCookies, privacyStatementText, cookieStatementText]);
35
93
  const handleSubmit = async (event) => {
36
94
  event.preventDefault();
37
95
  if (!onSubmit)
@@ -39,12 +97,14 @@ export function ProfileComponent({ onSubmit, submitText, showName = true, showPr
39
97
  setSaving(true);
40
98
  setErrorKey(null);
41
99
  setSuccessKey(null);
42
- const payload = {
100
+ const payload = Object.assign(Object.assign(Object.assign({}, (showName && {
43
101
  first_name: firstName,
44
102
  last_name: lastName,
103
+ })), (showPrivacy && {
45
104
  accepted_privacy_statement: acceptedPrivacy,
105
+ })), (showCookies && {
46
106
  accepted_convenience_cookies: acceptedCookies,
47
- };
107
+ }));
48
108
  try {
49
109
  await onSubmit(payload);
50
110
  setSuccessKey('Profile.UPDATE_SUCCESS');
@@ -68,5 +128,5 @@ export function ProfileComponent({ onSubmit, submitText, showName = true, showPr
68
128
  }
69
129
  const submitLabel = submitText || t('Profile.SAVE_BUTTON');
70
130
  const submitLabelLoading = t('Profile.SAVE_BUTTON_LOADING');
71
- return (_jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { maxWidth: 600, display: 'flex', flexDirection: 'column', gap: 2 }, children: [errorKey && (_jsx(Alert, { severity: "error", children: t(errorKey) })), successKey && (_jsx(Alert, { severity: "success", children: t(successKey) })), _jsxs(Stack, { spacing: 2, children: [_jsx(TextField, { label: t('Profile.USERNAME_LABEL'), value: username, fullWidth: true, disabled: true }), _jsx(TextField, { label: t('Auth.EMAIL_LABEL'), type: "email", value: email, fullWidth: true, disabled: true })] }), showName && (_jsxs(Stack, { spacing: 2, direction: { xs: 'column', sm: 'row' }, children: [_jsx(TextField, { label: t('Profile.FIRST_NAME_LABEL'), value: firstName, onChange: (e) => setFirstName(e.target.value), fullWidth: true }), _jsx(TextField, { label: t('Profile.LAST_NAME_LABEL'), 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: t('Profile.PRIVACY_SECTION_TITLE') }), _jsxs(Stack, { spacing: 1, children: [showPrivacy && (_jsx(FormControlLabel, { control: _jsx(Checkbox, { checked: acceptedPrivacy, onChange: (e) => setAcceptedPrivacy(e.target.checked) }), label: t('Profile.ACCEPT_PRIVACY_LABEL') })), showCookies && (_jsx(FormControlLabel, { control: _jsx(Checkbox, { checked: acceptedCookies, onChange: (e) => setAcceptedCookies(e.target.checked) }), label: t('Profile.ACCEPT_COOKIES_LABEL') }))] })] })), _jsx(Box, { sx: { mt: 2 }, children: _jsx(Button, { type: "submit", variant: "contained", disabled: saving, children: saving ? submitLabelLoading : submitLabel }) })] }));
131
+ return (_jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { maxWidth: 600, display: 'flex', flexDirection: 'column', gap: 2 }, children: [errorKey && (_jsx(Alert, { severity: "error", children: t(errorKey) })), successKey && (_jsx(Alert, { severity: "success", children: t(successKey) })), _jsxs(Stack, { spacing: 2, children: [_jsx(TextField, { label: t('Profile.USERNAME_LABEL'), value: username, fullWidth: true, disabled: true }), _jsx(TextField, { label: t('Auth.EMAIL_LABEL'), type: "email", value: email, fullWidth: true, disabled: true })] }), showName && (_jsxs(Stack, { spacing: 2, direction: { xs: 'column', sm: 'row' }, children: [_jsx(TextField, { label: t('Profile.FIRST_NAME_LABEL'), value: firstName, onChange: (e) => setFirstName(e.target.value), fullWidth: true }), _jsx(TextField, { label: t('Profile.LAST_NAME_LABEL'), 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: t('Profile.PRIVACY_SECTION_TITLE') }), _jsxs(Stack, { spacing: 1, children: [showPrivacy && (_jsx(FormControlLabel, { control: _jsx(Checkbox, { checked: acceptedPrivacy, onChange: (e) => setAcceptedPrivacy(e.target.checked) }), label: t('Profile.ACCEPT_PRIVACY_LABEL') })), showCookies && (_jsx(FormControlLabel, { control: _jsx(Checkbox, { checked: acceptedCookies, onChange: (e) => setAcceptedCookies(e.target.checked) }), label: t('Profile.ACCEPT_COOKIES_LABEL') }))] }), showStatements && (_jsxs(Stack, { spacing: 1.25, sx: { mt: 1.5 }, children: [showPrivacy && (_jsxs(Accordion, { disableGutters: true, children: [_jsx(AccordionSummary, { children: _jsx(Typography, { variant: "body2", fontWeight: 600, children: t('Profile.PRIVACY_STATEMENT_TITLE', 'Privacy statement') }) }), _jsx(AccordionDetails, { children: loadingPrivacyStatement ? (_jsx(Typography, { variant: "body2", color: "text.secondary", children: t('Profile.STATEMENT_LOADING', 'Loading statement...') })) : (_jsx(Typography, { variant: "body2", sx: { whiteSpace: 'pre-wrap' }, children: privacyStatement || t('Profile.PRIVACY_STATEMENT_EMPTY', 'Privacy statement is currently unavailable.') })) })] })), showCookies && (_jsxs(Accordion, { disableGutters: true, children: [_jsx(AccordionSummary, { children: _jsx(Typography, { variant: "body2", fontWeight: 600, children: t('Profile.COOKIES_STATEMENT_TITLE', 'Cookie statement') }) }), _jsx(AccordionDetails, { children: loadingCookieStatement ? (_jsx(Typography, { variant: "body2", color: "text.secondary", children: t('Profile.STATEMENT_LOADING', 'Loading statement...') })) : (_jsx(Typography, { variant: "body2", sx: { whiteSpace: 'pre-wrap' }, children: cookieStatement || t('Profile.COOKIES_STATEMENT_EMPTY', 'Cookie statement is currently unavailable.') })) })] }))] }))] })), _jsx(Box, { sx: { mt: 2 }, children: _jsx(Button, { type: "submit", variant: "contained", disabled: saving, children: saving ? submitLabelLoading : submitLabel }) })] }));
72
132
  }
@@ -1,6 +1,6 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // src/auth/components/SecurityComponent.jsx
3
- import React, { useState } from 'react';
3
+ import React, { useContext, useMemo, useState } from 'react';
4
4
  import { Box, Typography, Divider, Alert, } from '@mui/material';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import { PasswordChangeForm } from './PasswordChangeForm';
@@ -9,17 +9,34 @@ import { PasskeysComponent } from './PasskeysComponent';
9
9
  import { MFAComponent } from './MFAComponent';
10
10
  import { changePassword } from '../auth/authApi';
11
11
  import { startSocialLogin } from '../utils/authService';
12
- export function SecurityComponent({ fromRecovery = false, fromWeakLogin = false, // optional: wenn du später weak-login-Redirect nutzt
13
- }) {
12
+ import { AuthContext } from '../auth/AuthContext';
13
+ export function SecurityComponent({ fromRecovery = false, fromWeakLogin = false, }) {
14
14
  const { t } = useTranslation();
15
+ const { authMethods } = useContext(AuthContext);
15
16
  const [messageKey, setMessageKey] = useState(null);
16
17
  const [errorKey, setErrorKey] = useState(null);
18
+ const socialProviders = Array.isArray(authMethods === null || authMethods === void 0 ? void 0 : authMethods.social_providers)
19
+ ? authMethods.social_providers
20
+ : [];
21
+ const canChangePassword = Boolean(authMethods === null || authMethods === void 0 ? void 0 : authMethods.password_change);
22
+ const socialLoginEnabled = Boolean(authMethods === null || authMethods === void 0 ? void 0 : authMethods.social_login) && socialProviders.length > 0;
23
+ const passkeysEnabled = Boolean(authMethods === null || authMethods === void 0 ? void 0 : authMethods.passkeys_manage);
24
+ const mfaEnabled = Boolean(authMethods === null || authMethods === void 0 ? void 0 : authMethods.mfa_enabled);
25
+ const sectionOrder = useMemo(() => [
26
+ canChangePassword ? 'password' : null,
27
+ socialLoginEnabled ? 'social' : null,
28
+ passkeysEnabled ? 'passkeys' : null,
29
+ mfaEnabled ? 'mfa' : null,
30
+ ].filter(Boolean), [canChangePassword, socialLoginEnabled, passkeysEnabled, mfaEnabled]);
31
+ const needsDividerAfter = (section) => {
32
+ const idx = sectionOrder.indexOf(section);
33
+ return idx !== -1 && idx < sectionOrder.length - 1;
34
+ };
17
35
  const handleSocialClick = async (provider) => {
18
36
  setMessageKey(null);
19
37
  setErrorKey(null);
20
38
  try {
21
39
  await startSocialLogin(provider);
22
- // Redirect läuft über den Provider-Flow, hier kein extra Text nötig
23
40
  }
24
41
  catch (err) {
25
42
  setErrorKey(err.code || 'Auth.SOCIAL_LOGIN_FAILED');
@@ -36,6 +53,6 @@ export function SecurityComponent({ fromRecovery = false, fromWeakLogin = false,
36
53
  setErrorKey(err.code || 'Auth.PASSWORD_CHANGE_FAILED');
37
54
  }
38
55
  };
39
- return (_jsxs(Box, { children: [fromRecovery && (_jsx(Alert, { severity: "warning", sx: { mb: 2 }, children: t('Security.RECOVERY_LOGIN_WARNING') })), fromWeakLogin && (_jsx(Alert, { severity: "warning", sx: { mb: 2 }, children: t('Security.WEAK_LOGIN_WARNING') })), messageKey && (_jsx(Alert, { severity: "success", sx: { mb: 2 }, children: t(messageKey) })), errorKey && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(errorKey) })), _jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.LOGIN_PASSWORD_LABEL') }), _jsx(PasswordChangeForm, { onSubmit: handlePasswordChange }), _jsx(Divider, { sx: { my: 3 } }), _jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Security.SOCIAL_SECTION_TITLE') }), _jsx(Typography, { variant: "body2", sx: { mb: 1 }, children: t('Security.SOCIAL_SECTION_DESCRIPTION') }), _jsx(SocialLoginButtons, { onProviderClick: handleSocialClick }), _jsx(Divider, { sx: { my: 3 } }), _jsx(PasskeysComponent, {}), _jsx(Divider, { sx: { my: 3 } }), _jsx(MFAComponent, {})] }));
56
+ return (_jsxs(Box, { children: [fromRecovery && (_jsx(Alert, { severity: "warning", sx: { mb: 2 }, children: t('Security.RECOVERY_LOGIN_WARNING') })), fromWeakLogin && (_jsx(Alert, { severity: "warning", sx: { mb: 2 }, children: t('Security.WEAK_LOGIN_WARNING') })), messageKey && (_jsx(Alert, { severity: "success", sx: { mb: 2 }, children: t(messageKey) })), errorKey && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(errorKey) })), canChangePassword && (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.LOGIN_PASSWORD_LABEL') }), _jsx(PasswordChangeForm, { onSubmit: handlePasswordChange }), needsDividerAfter('password') && _jsx(Divider, { sx: { my: 3 } })] })), socialLoginEnabled && (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Security.SOCIAL_SECTION_TITLE') }), _jsx(Typography, { variant: "body2", sx: { mb: 1 }, children: t('Security.SOCIAL_SECTION_DESCRIPTION') }), _jsx(SocialLoginButtons, { onProviderClick: handleSocialClick, providers: socialProviders }), needsDividerAfter('social') && _jsx(Divider, { sx: { my: 3 } })] })), passkeysEnabled && (_jsxs(_Fragment, { children: [_jsx(PasskeysComponent, {}), needsDividerAfter('passkeys') && _jsx(Divider, { sx: { my: 3 } })] })), mfaEnabled && _jsx(MFAComponent, {})] }));
40
57
  }
41
58
  ;
@@ -8,14 +8,17 @@ import { SOCIAL_PROVIDERS } from '../auth/authConfig';
8
8
  * Renders buttons for social login providers.
9
9
  * The caller passes a handler that receives the provider key.
10
10
  */
11
- export function SocialLoginButtons({ onProviderClick }) {
11
+ export function SocialLoginButtons({ onProviderClick, providers }) {
12
12
  const { t } = useTranslation();
13
13
  const handleClick = (provider) => {
14
14
  if (onProviderClick) {
15
15
  onProviderClick(provider);
16
16
  }
17
17
  };
18
- return (_jsxs(Stack, { spacing: 1.5, children: [_jsx(Button, { variant: "outlined", fullWidth: true, onClick: () => handleClick(SOCIAL_PROVIDERS.google), startIcon: _jsx(Box, { sx: {
18
+ const activeProviders = Array.isArray(providers) && providers.length > 0
19
+ ? providers
20
+ : [SOCIAL_PROVIDERS.google, SOCIAL_PROVIDERS.microsoft];
21
+ return (_jsxs(Stack, { spacing: 1.5, children: [activeProviders.includes(SOCIAL_PROVIDERS.google) && (_jsx(Button, { variant: "outlined", fullWidth: true, onClick: () => handleClick(SOCIAL_PROVIDERS.google), startIcon: _jsx(Box, { sx: {
19
22
  width: 24,
20
23
  height: 24,
21
24
  borderRadius: '50%',
@@ -25,13 +28,13 @@ export function SocialLoginButtons({ onProviderClick }) {
25
28
  justifyContent: 'center',
26
29
  fontWeight: 700,
27
30
  fontSize: 14,
28
- }, children: "G" }), children: t('Auth.LOGIN_SOCIAL_GOOGLE') }), _jsx(Button, { variant: "outlined", fullWidth: true, onClick: () => handleClick(SOCIAL_PROVIDERS.microsoft), startIcon: _jsxs(Box, { sx: {
31
+ }, children: "G" }), children: t('Auth.LOGIN_SOCIAL_GOOGLE') })), activeProviders.includes(SOCIAL_PROVIDERS.microsoft) && (_jsx(Button, { variant: "outlined", fullWidth: true, onClick: () => handleClick(SOCIAL_PROVIDERS.microsoft), startIcon: _jsxs(Box, { sx: {
29
32
  width: 24,
30
33
  height: 24,
31
34
  display: 'grid',
32
35
  gridTemplateColumns: '1fr 1fr',
33
36
  gridTemplateRows: '1fr 1fr',
34
37
  gap: '1px',
35
- }, 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: t('Auth.LOGIN_SOCIAL_MICROSOFT') })] }));
38
+ }, 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: t('Auth.LOGIN_SOCIAL_MICROSOFT') }))] }));
36
39
  }
37
40
  ;
@@ -9,6 +9,12 @@ export function UserInviteComponent() {
9
9
  const [message, setMessage] = useState('');
10
10
  const [error, setError] = useState('');
11
11
  const [loading, setLoading] = useState(false);
12
+ const actionButtonSx = {
13
+ minWidth: 120,
14
+ height: 40,
15
+ textTransform: 'none',
16
+ whiteSpace: 'nowrap',
17
+ };
12
18
  const inviteUser = async () => {
13
19
  setMessage('');
14
20
  setError('');
@@ -32,8 +38,8 @@ export function UserInviteComponent() {
32
38
  setLoading(false);
33
39
  }
34
40
  };
35
- return (_jsxs(Box, { sx: { maxWidth: 600, mt: 2 }, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.INVITE_TITLE', 'Invite a new user') }), message && _jsx(Alert, { severity: "success", sx: { mb: 2 }, children: message }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error }), _jsxs(Box, { sx: { display: 'flex', gap: 2, alignItems: 'flex-start' }, children: [_jsx(TextField, { label: t('Auth.EMAIL_LABEL', 'Email address'), type: "email", variant: "outlined", fullWidth: true, size: "small", value: inviteEmail, onChange: (e) => setInviteEmail(e.target.value), disabled: loading, onKeyPress: (e) => {
41
+ return (_jsxs(Box, { sx: { maxWidth: 600 }, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.INVITE_TITLE', 'Invite a new user') }), message && _jsx(Alert, { severity: "success", sx: { mb: 2 }, children: message }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error }), _jsxs(Box, { sx: { display: 'flex', gap: 2, alignItems: 'center' }, children: [_jsx(TextField, { label: t('Auth.EMAIL_LABEL', 'Email address'), type: "email", variant: "outlined", fullWidth: true, size: "small", value: inviteEmail, onChange: (e) => setInviteEmail(e.target.value), disabled: loading, onKeyPress: (e) => {
36
42
  if (e.key === 'Enter')
37
43
  inviteUser();
38
- } }), _jsx(Button, { variant: "contained", onClick: inviteUser, disabled: loading || !inviteEmail, sx: { minWidth: 100, height: 40 }, children: loading ? _jsx(CircularProgress, { size: 24, color: "inherit" }) : t('Auth.INVITE_BUTTON', 'Invite') })] })] }));
44
+ } }), _jsx(Button, { variant: "contained", size: "small", onClick: inviteUser, disabled: loading || !inviteEmail, sx: actionButtonSx, children: loading ? _jsx(CircularProgress, { size: 24, color: "inherit" }) : t('Auth.INVITE_BUTTON', 'Invite') })] })] }));
39
45
  }