@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.
- package/dist/auth/AuthContext.js +34 -16
- package/dist/auth/authApi.js +19 -0
- package/dist/components/AccessCodeManager.js +8 -2
- package/dist/components/BulkInviteCsvTab.js +7 -1
- package/dist/components/LoginForm.js +2 -2
- package/dist/components/ProfileComponent.js +65 -5
- package/dist/components/SecurityComponent.js +23 -6
- package/dist/components/SocialLoginButtons.js +7 -4
- package/dist/components/UserInviteComponent.js +8 -2
- package/dist/components/UserListComponent.js +163 -21
- package/dist/i18n/authTranslations.js +30 -0
- package/dist/pages/AccountPage.js +11 -22
- package/dist/pages/LoginPage.js +16 -2
- package/dist/utils/authService.js +41 -2
- package/package.json +2 -1
- package/src/auth/AuthContext.jsx +64 -16
- package/src/auth/authApi.jsx +22 -1
- package/src/components/AccessCodeManager.jsx +14 -4
- package/src/components/BulkInviteCsvTab.jsx +9 -3
- package/src/components/LoginForm.jsx +58 -48
- package/src/components/ProfileComponent.jsx +117 -4
- package/src/components/SecurityComponent.jsx +58 -28
- package/src/components/SocialLoginButtons.jsx +57 -49
- package/src/components/UserInviteComponent.jsx +11 -4
- package/src/components/UserListComponent.jsx +252 -40
- package/src/i18n/authTranslations.ts +31 -1
- package/src/pages/AccountPage.jsx +34 -39
- package/src/pages/LoginPage.jsx +25 -6
- package/src/utils/authService.js +51 -3
|
@@ -23,6 +23,12 @@ import {
|
|
|
23
23
|
|
|
24
24
|
export function AccessCodeManager() {
|
|
25
25
|
const { t } = useTranslation();
|
|
26
|
+
const actionButtonSx = {
|
|
27
|
+
minWidth: 120,
|
|
28
|
+
height: 40,
|
|
29
|
+
textTransform: 'none',
|
|
30
|
+
whiteSpace: 'nowrap',
|
|
31
|
+
};
|
|
26
32
|
|
|
27
33
|
const [codes, setCodes] = useState([]);
|
|
28
34
|
const [loading, setLoading] = useState(true);
|
|
@@ -118,7 +124,7 @@ export function AccessCodeManager() {
|
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
return (
|
|
121
|
-
<Box
|
|
127
|
+
<Box>
|
|
122
128
|
{errorKey && (
|
|
123
129
|
<Alert severity="error" sx={{ mb: 2 }}>
|
|
124
130
|
{t(errorKey)}
|
|
@@ -176,7 +182,8 @@ export function AccessCodeManager() {
|
|
|
176
182
|
|
|
177
183
|
<Button
|
|
178
184
|
variant="contained"
|
|
179
|
-
|
|
185
|
+
size="small"
|
|
186
|
+
sx={{ ...actionButtonSx, mt: 1 }}
|
|
180
187
|
onClick={handleGenerateClick}
|
|
181
188
|
disabled={submitting}
|
|
182
189
|
>
|
|
@@ -191,18 +198,21 @@ export function AccessCodeManager() {
|
|
|
191
198
|
<Typography variant="subtitle1" gutterBottom>
|
|
192
199
|
{t('Auth.ACCESS_CODE_SECTION_MANUAL')}
|
|
193
200
|
</Typography>
|
|
194
|
-
<Box sx={{ display: 'flex', gap: 1 }}>
|
|
201
|
+
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
|
195
202
|
<TextField
|
|
196
203
|
label={t('Auth.ACCESS_CODE_LABEL')}
|
|
197
204
|
fullWidth
|
|
205
|
+
size="small"
|
|
198
206
|
value={manualCode}
|
|
199
207
|
onChange={(e) => setManualCode(e.target.value)}
|
|
200
208
|
disabled={submitting}
|
|
201
209
|
/>
|
|
202
210
|
<Button
|
|
203
|
-
variant="
|
|
211
|
+
variant="contained"
|
|
212
|
+
size="small"
|
|
204
213
|
onClick={handleAddManual}
|
|
205
214
|
disabled={submitting}
|
|
215
|
+
sx={actionButtonSx}
|
|
206
216
|
>
|
|
207
217
|
{t('Auth.ACCESS_CODE_ADD_BUTTON')}
|
|
208
218
|
</Button>
|
|
@@ -45,6 +45,12 @@ export function BulkInviteCsvTab({
|
|
|
45
45
|
onCompleted,
|
|
46
46
|
}) {
|
|
47
47
|
const { t } = useTranslation();
|
|
48
|
+
const actionButtonSx = {
|
|
49
|
+
minWidth: 120,
|
|
50
|
+
height: 40,
|
|
51
|
+
textTransform: 'none',
|
|
52
|
+
whiteSpace: 'nowrap',
|
|
53
|
+
};
|
|
48
54
|
const [emails, setEmails] = useState([]);
|
|
49
55
|
const [results, setResults] = useState({});
|
|
50
56
|
const [busy, setBusy] = useState(false);
|
|
@@ -117,7 +123,7 @@ export function BulkInviteCsvTab({
|
|
|
117
123
|
};
|
|
118
124
|
|
|
119
125
|
return (
|
|
120
|
-
<Box
|
|
126
|
+
<Box>
|
|
121
127
|
<Typography variant="h6" gutterBottom>
|
|
122
128
|
{t('Account.BULK_INVITE_TITLE', 'Bulk Invite via CSV')}
|
|
123
129
|
</Typography>
|
|
@@ -132,11 +138,11 @@ export function BulkInviteCsvTab({
|
|
|
132
138
|
{success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
|
|
133
139
|
|
|
134
140
|
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', mb: 2, flexWrap: 'wrap' }}>
|
|
135
|
-
<Button variant="outlined" component="label" disabled={busy}>
|
|
141
|
+
<Button variant="outlined" size="small" component="label" disabled={busy} sx={actionButtonSx}>
|
|
136
142
|
{t('Account.BULK_INVITE_UPLOAD', 'Upload CSV')}
|
|
137
143
|
<input type="file" accept=".csv,text/csv" hidden onChange={handleFile} />
|
|
138
144
|
</Button>
|
|
139
|
-
<Button variant="contained" onClick={handleInviteAll} disabled={busy || emails.length === 0}>
|
|
145
|
+
<Button variant="contained" size="small" sx={actionButtonSx} onClick={handleInviteAll} disabled={busy || emails.length === 0}>
|
|
140
146
|
{t('Account.BULK_INVITE_SEND', 'Send Invites')}
|
|
141
147
|
</Button>
|
|
142
148
|
<Typography variant="body2">
|
|
@@ -13,6 +13,7 @@ export function LoginForm({
|
|
|
13
13
|
onSubmit,
|
|
14
14
|
onForgotPassword,
|
|
15
15
|
onSocialLogin,
|
|
16
|
+
socialProviders,
|
|
16
17
|
onPasskeyLogin,
|
|
17
18
|
onSignUp,
|
|
18
19
|
error, // bereits übersetzter Text oder t(errorKey) aus dem Parent
|
|
@@ -68,52 +69,58 @@ export function LoginForm({
|
|
|
68
69
|
)}
|
|
69
70
|
|
|
70
71
|
{/* Sign in: E-Mail + Passwort */}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
72
|
+
{onSubmit && (
|
|
73
|
+
<Box
|
|
74
|
+
component="form"
|
|
75
|
+
onSubmit={handleSubmit}
|
|
76
|
+
sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}
|
|
77
|
+
>
|
|
78
|
+
<TextField
|
|
79
|
+
label={t('Auth.EMAIL_LABEL')}
|
|
80
|
+
type="email"
|
|
81
|
+
required
|
|
82
|
+
fullWidth
|
|
83
|
+
value={identifier}
|
|
84
|
+
onChange={(e) => setIdentifier(e.target.value)}
|
|
85
|
+
disabled={disabled}
|
|
86
|
+
/>
|
|
85
87
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
88
|
+
<TextField
|
|
89
|
+
label={t('Auth.LOGIN_PASSWORD_LABEL')}
|
|
90
|
+
type="password"
|
|
91
|
+
required
|
|
92
|
+
fullWidth
|
|
93
|
+
value={password}
|
|
94
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
95
|
+
disabled={disabled}
|
|
96
|
+
/>
|
|
95
97
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
98
|
+
<Button
|
|
99
|
+
type="submit"
|
|
100
|
+
variant="contained"
|
|
101
|
+
fullWidth
|
|
102
|
+
disabled={disabled}
|
|
103
|
+
>
|
|
104
|
+
{t('Auth.PAGE_LOGIN_TITLE')}
|
|
105
|
+
</Button>
|
|
106
|
+
</Box>
|
|
107
|
+
)}
|
|
105
108
|
|
|
106
109
|
{/* Other ways to sign in */}
|
|
107
|
-
{
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
{onSocialLogin && (
|
|
111
|
+
<Box>
|
|
112
|
+
<Divider sx={{ my: 2 }}>
|
|
113
|
+
{t('Auth.LOGIN_OR')}
|
|
114
|
+
</Divider>
|
|
115
|
+
<SocialLoginButtons
|
|
116
|
+
onProviderClick={onSocialLogin}
|
|
117
|
+
providers={socialProviders}
|
|
118
|
+
/>
|
|
119
|
+
</Box>
|
|
120
|
+
)}
|
|
115
121
|
{/* Account & Recovery */}
|
|
116
122
|
|
|
123
|
+
{(onSignUp || onForgotPassword) && (
|
|
117
124
|
<Box>
|
|
118
125
|
<Typography variant="subtitle2" sx={{ mb: 1 }}>
|
|
119
126
|
{t('Auth.LOGIN_ACCOUNT_RECOVERY_TITLE')}
|
|
@@ -131,16 +138,19 @@ export function LoginForm({
|
|
|
131
138
|
</Button>
|
|
132
139
|
)}
|
|
133
140
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
{onForgotPassword && (
|
|
142
|
+
<Button
|
|
143
|
+
type="button"
|
|
144
|
+
variant="outlined"
|
|
145
|
+
onClick={onForgotPassword}
|
|
146
|
+
disabled={disabled}
|
|
147
|
+
>
|
|
148
|
+
{t('Auth.LOGIN_FORGOT_PASSWORD_BUTTON')}
|
|
149
|
+
</Button>
|
|
150
|
+
)}
|
|
142
151
|
</Box>
|
|
143
152
|
</Box>
|
|
153
|
+
)}
|
|
144
154
|
</Box>
|
|
145
155
|
);
|
|
146
156
|
};
|
|
@@ -10,10 +10,14 @@ import {
|
|
|
10
10
|
CircularProgress,
|
|
11
11
|
Alert,
|
|
12
12
|
Typography,
|
|
13
|
+
Accordion,
|
|
14
|
+
AccordionSummary,
|
|
15
|
+
AccordionDetails,
|
|
13
16
|
} from '@mui/material';
|
|
14
17
|
import { useTranslation } from 'react-i18next';
|
|
15
18
|
// WICHTIG: Importiere den Context, um die bereits geladenen Daten zu nutzen
|
|
16
19
|
import { AuthContext } from '../auth/AuthContext';
|
|
20
|
+
import { fetchCookieStatement, fetchPrivacyStatement } from '../auth/authApi';
|
|
17
21
|
|
|
18
22
|
export function ProfileComponent({
|
|
19
23
|
onSubmit,
|
|
@@ -21,6 +25,9 @@ export function ProfileComponent({
|
|
|
21
25
|
showName = true,
|
|
22
26
|
showPrivacy = true,
|
|
23
27
|
showCookies = true,
|
|
28
|
+
showStatements = true,
|
|
29
|
+
privacyStatementText = null,
|
|
30
|
+
cookieStatementText = null,
|
|
24
31
|
}) {
|
|
25
32
|
const { t } = useTranslation();
|
|
26
33
|
|
|
@@ -39,6 +46,10 @@ export function ProfileComponent({
|
|
|
39
46
|
const [lastName, setLastName] = useState('');
|
|
40
47
|
const [acceptedPrivacy, setAcceptedPrivacy] = useState(false);
|
|
41
48
|
const [acceptedCookies, setAcceptedCookies] = useState(false);
|
|
49
|
+
const [privacyStatement, setPrivacyStatement] = useState(privacyStatementText || '');
|
|
50
|
+
const [cookieStatement, setCookieStatement] = useState(cookieStatementText || '');
|
|
51
|
+
const [loadingPrivacyStatement, setLoadingPrivacyStatement] = useState(false);
|
|
52
|
+
const [loadingCookieStatement, setLoadingCookieStatement] = useState(false);
|
|
42
53
|
|
|
43
54
|
// Synchronisiere Formular-Daten, sobald der User aus dem Context da ist
|
|
44
55
|
useEffect(() => {
|
|
@@ -52,6 +63,56 @@ export function ProfileComponent({
|
|
|
52
63
|
}
|
|
53
64
|
}, [user]);
|
|
54
65
|
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (privacyStatementText !== null && privacyStatementText !== undefined) {
|
|
68
|
+
setPrivacyStatement(String(privacyStatementText));
|
|
69
|
+
}
|
|
70
|
+
}, [privacyStatementText]);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (cookieStatementText !== null && cookieStatementText !== undefined) {
|
|
74
|
+
setCookieStatement(String(cookieStatementText));
|
|
75
|
+
}
|
|
76
|
+
}, [cookieStatementText]);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
let cancelled = false;
|
|
80
|
+
|
|
81
|
+
const loadStatements = async () => {
|
|
82
|
+
if (!showStatements) return;
|
|
83
|
+
|
|
84
|
+
if (showPrivacy && (privacyStatementText === null || privacyStatementText === undefined)) {
|
|
85
|
+
setLoadingPrivacyStatement(true);
|
|
86
|
+
try {
|
|
87
|
+
const text = await fetchPrivacyStatement();
|
|
88
|
+
if (!cancelled) setPrivacyStatement(text || '');
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (!cancelled) setPrivacyStatement('');
|
|
91
|
+
} finally {
|
|
92
|
+
if (!cancelled) setLoadingPrivacyStatement(false);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (showCookies && (cookieStatementText === null || cookieStatementText === undefined)) {
|
|
97
|
+
setLoadingCookieStatement(true);
|
|
98
|
+
try {
|
|
99
|
+
const text = await fetchCookieStatement();
|
|
100
|
+
if (!cancelled) setCookieStatement(text || '');
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (!cancelled) setCookieStatement('');
|
|
103
|
+
} finally {
|
|
104
|
+
if (!cancelled) setLoadingCookieStatement(false);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
loadStatements();
|
|
110
|
+
|
|
111
|
+
return () => {
|
|
112
|
+
cancelled = true;
|
|
113
|
+
};
|
|
114
|
+
}, [showStatements, showPrivacy, showCookies, privacyStatementText, cookieStatementText]);
|
|
115
|
+
|
|
55
116
|
const handleSubmit = async (event) => {
|
|
56
117
|
event.preventDefault();
|
|
57
118
|
if (!onSubmit) return;
|
|
@@ -61,10 +122,16 @@ export function ProfileComponent({
|
|
|
61
122
|
setSuccessKey(null);
|
|
62
123
|
|
|
63
124
|
const payload = {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
125
|
+
...(showName && {
|
|
126
|
+
first_name: firstName,
|
|
127
|
+
last_name: lastName,
|
|
128
|
+
}),
|
|
129
|
+
...(showPrivacy && {
|
|
130
|
+
accepted_privacy_statement: acceptedPrivacy,
|
|
131
|
+
}),
|
|
132
|
+
...(showCookies && {
|
|
133
|
+
accepted_convenience_cookies: acceptedCookies,
|
|
134
|
+
}),
|
|
68
135
|
};
|
|
69
136
|
|
|
70
137
|
try {
|
|
@@ -183,6 +250,52 @@ export function ProfileComponent({
|
|
|
183
250
|
/>
|
|
184
251
|
)}
|
|
185
252
|
</Stack>
|
|
253
|
+
|
|
254
|
+
{showStatements && (
|
|
255
|
+
<Stack spacing={1.25} sx={{ mt: 1.5 }}>
|
|
256
|
+
{showPrivacy && (
|
|
257
|
+
<Accordion disableGutters>
|
|
258
|
+
<AccordionSummary>
|
|
259
|
+
<Typography variant="body2" fontWeight={600}>
|
|
260
|
+
{t('Profile.PRIVACY_STATEMENT_TITLE', 'Privacy statement')}
|
|
261
|
+
</Typography>
|
|
262
|
+
</AccordionSummary>
|
|
263
|
+
<AccordionDetails>
|
|
264
|
+
{loadingPrivacyStatement ? (
|
|
265
|
+
<Typography variant="body2" color="text.secondary">
|
|
266
|
+
{t('Profile.STATEMENT_LOADING', 'Loading statement...')}
|
|
267
|
+
</Typography>
|
|
268
|
+
) : (
|
|
269
|
+
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
|
|
270
|
+
{privacyStatement || t('Profile.PRIVACY_STATEMENT_EMPTY', 'Privacy statement is currently unavailable.')}
|
|
271
|
+
</Typography>
|
|
272
|
+
)}
|
|
273
|
+
</AccordionDetails>
|
|
274
|
+
</Accordion>
|
|
275
|
+
)}
|
|
276
|
+
|
|
277
|
+
{showCookies && (
|
|
278
|
+
<Accordion disableGutters>
|
|
279
|
+
<AccordionSummary>
|
|
280
|
+
<Typography variant="body2" fontWeight={600}>
|
|
281
|
+
{t('Profile.COOKIES_STATEMENT_TITLE', 'Cookie statement')}
|
|
282
|
+
</Typography>
|
|
283
|
+
</AccordionSummary>
|
|
284
|
+
<AccordionDetails>
|
|
285
|
+
{loadingCookieStatement ? (
|
|
286
|
+
<Typography variant="body2" color="text.secondary">
|
|
287
|
+
{t('Profile.STATEMENT_LOADING', 'Loading statement...')}
|
|
288
|
+
</Typography>
|
|
289
|
+
) : (
|
|
290
|
+
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
|
|
291
|
+
{cookieStatement || t('Profile.COOKIES_STATEMENT_EMPTY', 'Cookie statement is currently unavailable.')}
|
|
292
|
+
</Typography>
|
|
293
|
+
)}
|
|
294
|
+
</AccordionDetails>
|
|
295
|
+
</Accordion>
|
|
296
|
+
)}
|
|
297
|
+
</Stack>
|
|
298
|
+
)}
|
|
186
299
|
</Box>
|
|
187
300
|
)}
|
|
188
301
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/auth/components/SecurityComponent.jsx
|
|
2
|
-
import React, { useState } from 'react';
|
|
2
|
+
import React, { useContext, useMemo, useState } from 'react';
|
|
3
3
|
import {
|
|
4
4
|
Box,
|
|
5
5
|
Typography,
|
|
@@ -13,22 +13,46 @@ import { PasskeysComponent } from './PasskeysComponent';
|
|
|
13
13
|
import { MFAComponent } from './MFAComponent';
|
|
14
14
|
import { changePassword } from '../auth/authApi';
|
|
15
15
|
import { startSocialLogin } from '../utils/authService';
|
|
16
|
+
import { AuthContext } from '../auth/AuthContext';
|
|
16
17
|
|
|
17
18
|
export function SecurityComponent({
|
|
18
19
|
fromRecovery = false,
|
|
19
|
-
fromWeakLogin = false,
|
|
20
|
+
fromWeakLogin = false,
|
|
20
21
|
}) {
|
|
21
22
|
const { t } = useTranslation();
|
|
23
|
+
const { authMethods } = useContext(AuthContext);
|
|
22
24
|
|
|
23
25
|
const [messageKey, setMessageKey] = useState(null);
|
|
24
26
|
const [errorKey, setErrorKey] = useState(null);
|
|
25
27
|
|
|
28
|
+
const socialProviders = Array.isArray(authMethods?.social_providers)
|
|
29
|
+
? authMethods.social_providers
|
|
30
|
+
: [];
|
|
31
|
+
const canChangePassword = Boolean(authMethods?.password_change);
|
|
32
|
+
const socialLoginEnabled = Boolean(authMethods?.social_login) && socialProviders.length > 0;
|
|
33
|
+
const passkeysEnabled = Boolean(authMethods?.passkeys_manage);
|
|
34
|
+
const mfaEnabled = Boolean(authMethods?.mfa_enabled);
|
|
35
|
+
|
|
36
|
+
const sectionOrder = useMemo(
|
|
37
|
+
() => [
|
|
38
|
+
canChangePassword ? 'password' : null,
|
|
39
|
+
socialLoginEnabled ? 'social' : null,
|
|
40
|
+
passkeysEnabled ? 'passkeys' : null,
|
|
41
|
+
mfaEnabled ? 'mfa' : null,
|
|
42
|
+
].filter(Boolean),
|
|
43
|
+
[canChangePassword, socialLoginEnabled, passkeysEnabled, mfaEnabled],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const needsDividerAfter = (section) => {
|
|
47
|
+
const idx = sectionOrder.indexOf(section);
|
|
48
|
+
return idx !== -1 && idx < sectionOrder.length - 1;
|
|
49
|
+
};
|
|
50
|
+
|
|
26
51
|
const handleSocialClick = async (provider) => {
|
|
27
52
|
setMessageKey(null);
|
|
28
53
|
setErrorKey(null);
|
|
29
54
|
try {
|
|
30
55
|
await startSocialLogin(provider);
|
|
31
|
-
// Redirect läuft über den Provider-Flow, hier kein extra Text nötig
|
|
32
56
|
} catch (err) {
|
|
33
57
|
setErrorKey(err.code || 'Auth.SOCIAL_LOGIN_FAILED');
|
|
34
58
|
}
|
|
@@ -47,14 +71,12 @@ export function SecurityComponent({
|
|
|
47
71
|
|
|
48
72
|
return (
|
|
49
73
|
<Box>
|
|
50
|
-
{/* Hinweis nach Recovery-Login */}
|
|
51
74
|
{fromRecovery && (
|
|
52
75
|
<Alert severity="warning" sx={{ mb: 2 }}>
|
|
53
76
|
{t('Security.RECOVERY_LOGIN_WARNING')}
|
|
54
77
|
</Alert>
|
|
55
78
|
)}
|
|
56
79
|
|
|
57
|
-
{/* Optional: Hinweis nach „schwachem“ Login, wenn du das nutzt */}
|
|
58
80
|
{fromWeakLogin && (
|
|
59
81
|
<Alert severity="warning" sx={{ mb: 2 }}>
|
|
60
82
|
{t('Security.WEAK_LOGIN_WARNING')}
|
|
@@ -72,32 +94,40 @@ export function SecurityComponent({
|
|
|
72
94
|
</Alert>
|
|
73
95
|
)}
|
|
74
96
|
|
|
75
|
-
{
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
<Typography variant="h6" gutterBottom>
|
|
85
|
-
{t('Security.SOCIAL_SECTION_TITLE')}
|
|
86
|
-
</Typography>
|
|
87
|
-
<Typography variant="body2" sx={{ mb: 1 }}>
|
|
88
|
-
{t('Security.SOCIAL_SECTION_DESCRIPTION')}
|
|
89
|
-
</Typography>
|
|
90
|
-
<SocialLoginButtons onProviderClick={handleSocialClick} />
|
|
91
|
-
|
|
92
|
-
<Divider sx={{ my: 3 }} />
|
|
97
|
+
{canChangePassword && (
|
|
98
|
+
<>
|
|
99
|
+
<Typography variant="h6" gutterBottom>
|
|
100
|
+
{t('Auth.LOGIN_PASSWORD_LABEL')}
|
|
101
|
+
</Typography>
|
|
102
|
+
<PasswordChangeForm onSubmit={handlePasswordChange} />
|
|
103
|
+
{needsDividerAfter('password') && <Divider sx={{ my: 3 }} />}
|
|
104
|
+
</>
|
|
105
|
+
)}
|
|
93
106
|
|
|
94
|
-
{
|
|
95
|
-
|
|
107
|
+
{socialLoginEnabled && (
|
|
108
|
+
<>
|
|
109
|
+
<Typography variant="h6" gutterBottom>
|
|
110
|
+
{t('Security.SOCIAL_SECTION_TITLE')}
|
|
111
|
+
</Typography>
|
|
112
|
+
<Typography variant="body2" sx={{ mb: 1 }}>
|
|
113
|
+
{t('Security.SOCIAL_SECTION_DESCRIPTION')}
|
|
114
|
+
</Typography>
|
|
115
|
+
<SocialLoginButtons
|
|
116
|
+
onProviderClick={handleSocialClick}
|
|
117
|
+
providers={socialProviders}
|
|
118
|
+
/>
|
|
119
|
+
{needsDividerAfter('social') && <Divider sx={{ my: 3 }} />}
|
|
120
|
+
</>
|
|
121
|
+
)}
|
|
96
122
|
|
|
97
|
-
|
|
123
|
+
{passkeysEnabled && (
|
|
124
|
+
<>
|
|
125
|
+
<PasskeysComponent />
|
|
126
|
+
{needsDividerAfter('passkeys') && <Divider sx={{ my: 3 }} />}
|
|
127
|
+
</>
|
|
128
|
+
)}
|
|
98
129
|
|
|
99
|
-
{
|
|
100
|
-
<MFAComponent />
|
|
130
|
+
{mfaEnabled && <MFAComponent />}
|
|
101
131
|
</Box>
|
|
102
132
|
);
|
|
103
133
|
};
|
|
@@ -8,7 +8,7 @@ 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
|
|
|
14
14
|
const handleClick = (provider) => {
|
|
@@ -17,57 +17,65 @@ export function SocialLoginButtons({ onProviderClick }) {
|
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
const activeProviders = Array.isArray(providers) && providers.length > 0
|
|
21
|
+
? providers
|
|
22
|
+
: [SOCIAL_PROVIDERS.google, SOCIAL_PROVIDERS.microsoft];
|
|
23
|
+
|
|
20
24
|
return (
|
|
21
25
|
<Stack spacing={1.5}>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
26
|
+
{activeProviders.includes(SOCIAL_PROVIDERS.google) && (
|
|
27
|
+
<Button
|
|
28
|
+
variant="outlined"
|
|
29
|
+
fullWidth
|
|
30
|
+
onClick={() => handleClick(SOCIAL_PROVIDERS.google)}
|
|
31
|
+
startIcon={
|
|
32
|
+
<Box
|
|
33
|
+
sx={{
|
|
34
|
+
width: 24,
|
|
35
|
+
height: 24,
|
|
36
|
+
borderRadius: '50%',
|
|
37
|
+
border: '1px solid rgba(0,0,0,0.2)',
|
|
38
|
+
display: 'flex',
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
justifyContent: 'center',
|
|
41
|
+
fontWeight: 700,
|
|
42
|
+
fontSize: 14,
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
G
|
|
46
|
+
</Box>
|
|
47
|
+
}
|
|
48
|
+
>
|
|
49
|
+
{t('Auth.LOGIN_SOCIAL_GOOGLE')}
|
|
50
|
+
</Button>
|
|
51
|
+
)}
|
|
46
52
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
53
|
+
{activeProviders.includes(SOCIAL_PROVIDERS.microsoft) && (
|
|
54
|
+
<Button
|
|
55
|
+
variant="outlined"
|
|
56
|
+
fullWidth
|
|
57
|
+
onClick={() => handleClick(SOCIAL_PROVIDERS.microsoft)}
|
|
58
|
+
startIcon={
|
|
59
|
+
<Box
|
|
60
|
+
sx={{
|
|
61
|
+
width: 24,
|
|
62
|
+
height: 24,
|
|
63
|
+
display: 'grid',
|
|
64
|
+
gridTemplateColumns: '1fr 1fr',
|
|
65
|
+
gridTemplateRows: '1fr 1fr',
|
|
66
|
+
gap: '1px',
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
<Box sx={{ bgcolor: 'primary.main', opacity: 0.9 }} />
|
|
70
|
+
<Box sx={{ bgcolor: 'primary.main', opacity: 0.7 }} />
|
|
71
|
+
<Box sx={{ bgcolor: 'primary.main', opacity: 0.7 }} />
|
|
72
|
+
<Box sx={{ bgcolor: 'primary.main', opacity: 0.9 }} />
|
|
73
|
+
</Box>
|
|
74
|
+
}
|
|
75
|
+
>
|
|
76
|
+
{t('Auth.LOGIN_SOCIAL_MICROSOFT')}
|
|
77
|
+
</Button>
|
|
78
|
+
)}
|
|
71
79
|
</Stack>
|
|
72
80
|
);
|
|
73
81
|
};
|