@micha.bigler/ui-core-micha 2.2.14 → 2.3.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/components/AccessCodeSingleUseToggle.js +59 -0
- package/dist/components/EmailVerificationRequirementCard.js +59 -0
- package/dist/i18n/authTranslations.js +48 -0
- package/dist/index.js +2 -0
- package/dist/pages/AccountPage.js +3 -1
- package/package.json +2 -2
- package/src/components/AccessCodeSingleUseToggle.jsx +89 -0
- package/src/components/EmailVerificationRequirementCard.jsx +89 -0
- package/src/i18n/authTranslations.ts +48 -0
- package/src/index.js +2 -0
- package/src/pages/AccountPage.jsx +22 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useEffect, useState } from 'react';
|
|
3
|
+
import { Alert, Box, FormControlLabel, Switch, Typography, } from '@mui/material';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { fetchAuthPolicy, updateAuthPolicy } from '../auth/authApi';
|
|
6
|
+
export function AccessCodeSingleUseToggle({ canEdit = true, policy = null, onPolicyChange = null }) {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
const [value, setValue] = useState(false);
|
|
9
|
+
const [busy, setBusy] = useState(false);
|
|
10
|
+
const [error, setError] = useState('');
|
|
11
|
+
const [success, setSuccess] = useState('');
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (policy) {
|
|
14
|
+
setValue(Boolean(policy === null || policy === void 0 ? void 0 : policy.access_code_single_use));
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
let active = true;
|
|
18
|
+
(async () => {
|
|
19
|
+
try {
|
|
20
|
+
const data = await fetchAuthPolicy();
|
|
21
|
+
if (active) {
|
|
22
|
+
setValue(Boolean(data === null || data === void 0 ? void 0 : data.access_code_single_use));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (_a) {
|
|
26
|
+
// Keep defaults when policy is unavailable.
|
|
27
|
+
}
|
|
28
|
+
})();
|
|
29
|
+
return () => {
|
|
30
|
+
active = false;
|
|
31
|
+
};
|
|
32
|
+
}, [policy]);
|
|
33
|
+
const handleChange = async (event) => {
|
|
34
|
+
if (!canEdit) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const nextValue = event.target.checked;
|
|
38
|
+
const previous = value;
|
|
39
|
+
setValue(nextValue);
|
|
40
|
+
setBusy(true);
|
|
41
|
+
setError('');
|
|
42
|
+
setSuccess('');
|
|
43
|
+
try {
|
|
44
|
+
const updated = await updateAuthPolicy({ access_code_single_use: nextValue });
|
|
45
|
+
if (onPolicyChange) {
|
|
46
|
+
onPolicyChange((current) => (Object.assign(Object.assign({}, (current || {})), (updated || {}))));
|
|
47
|
+
}
|
|
48
|
+
setSuccess(t('Auth.ACCESS_CODE_SINGLE_USE_SAVE_SUCCESS', 'Access code policy saved.'));
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
setValue(previous);
|
|
52
|
+
setError(t((err === null || err === void 0 ? void 0 : err.code) || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save access code policy.'));
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
setBusy(false);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.ACCESS_CODE_SINGLE_USE_TITLE', 'Access Code Single-Use') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Auth.ACCESS_CODE_SINGLE_USE_HINT', 'When enabled, each access code can be redeemed only once. Recommended.') }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error }), success && _jsx(Alert, { severity: "success", sx: { mb: 2 }, children: success }), _jsx(FormControlLabel, { control: _jsx(Switch, { checked: value, disabled: busy || !canEdit, onChange: handleChange }), label: t('Auth.ACCESS_CODE_SINGLE_USE_TOGGLE', 'Codes are single-use') })] }));
|
|
59
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useEffect, useState } from 'react';
|
|
3
|
+
import { Alert, Box, FormControlLabel, Switch, Typography, } from '@mui/material';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { fetchAuthPolicy, updateAuthPolicy } from '../auth/authApi';
|
|
6
|
+
export function EmailVerificationRequirementCard({ canEdit = true, policy = null, onPolicyChange = null }) {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
const [value, setValue] = useState(false);
|
|
9
|
+
const [busy, setBusy] = useState(false);
|
|
10
|
+
const [error, setError] = useState('');
|
|
11
|
+
const [success, setSuccess] = useState('');
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (policy) {
|
|
14
|
+
setValue(Boolean(policy === null || policy === void 0 ? void 0 : policy.require_email_verification));
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
let active = true;
|
|
18
|
+
(async () => {
|
|
19
|
+
try {
|
|
20
|
+
const data = await fetchAuthPolicy();
|
|
21
|
+
if (active) {
|
|
22
|
+
setValue(Boolean(data === null || data === void 0 ? void 0 : data.require_email_verification));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (_a) {
|
|
26
|
+
// Keep defaults when policy is unavailable.
|
|
27
|
+
}
|
|
28
|
+
})();
|
|
29
|
+
return () => {
|
|
30
|
+
active = false;
|
|
31
|
+
};
|
|
32
|
+
}, [policy]);
|
|
33
|
+
const handleChange = async (event) => {
|
|
34
|
+
if (!canEdit) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const nextValue = event.target.checked;
|
|
38
|
+
const previous = value;
|
|
39
|
+
setValue(nextValue);
|
|
40
|
+
setBusy(true);
|
|
41
|
+
setError('');
|
|
42
|
+
setSuccess('');
|
|
43
|
+
try {
|
|
44
|
+
const updated = await updateAuthPolicy({ require_email_verification: nextValue });
|
|
45
|
+
if (onPolicyChange) {
|
|
46
|
+
onPolicyChange((current) => (Object.assign(Object.assign({}, (current || {})), (updated || {}))));
|
|
47
|
+
}
|
|
48
|
+
setSuccess(t('Auth.EMAIL_VERIFICATION_SAVE_SUCCESS', 'Email verification requirement saved.'));
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
setValue(previous);
|
|
52
|
+
setError(t((err === null || err === void 0 ? void 0 : err.code) || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save email verification requirement.'));
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
setBusy(false);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.EMAIL_VERIFICATION_TITLE', 'Email Verification') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Auth.EMAIL_VERIFICATION_HINT', 'Require verified email ownership before social sign-in can link to an account. Recommended.') }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error }), success && _jsx(Alert, { severity: "success", sx: { mb: 2 }, children: success }), _jsx(FormControlLabel, { control: _jsx(Switch, { checked: value, disabled: busy || !canEdit, onChange: handleChange }), label: t('Auth.EMAIL_VERIFICATION_TOGGLE', 'Require email verification') })] }));
|
|
59
|
+
}
|
|
@@ -1643,5 +1643,53 @@ export const authTranslations = {
|
|
|
1643
1643
|
"fr": "Chargement…",
|
|
1644
1644
|
"en": "Loading…",
|
|
1645
1645
|
"sw": "Inapakia..."
|
|
1646
|
+
},
|
|
1647
|
+
"Auth.EMAIL_VERIFICATION_TITLE": {
|
|
1648
|
+
"de": "E-Mail-Verifizierung",
|
|
1649
|
+
"fr": "Vérification de l'e-mail",
|
|
1650
|
+
"en": "Email Verification",
|
|
1651
|
+
"sw": "Uthibitishaji wa Barua Pepe"
|
|
1652
|
+
},
|
|
1653
|
+
"Auth.EMAIL_VERIFICATION_HINT": {
|
|
1654
|
+
"de": "Verlangt einen Nachweis des E-Mail-Besitzes, bevor ein Social-Login mit einem Konto verknüpft werden kann. Empfohlen.",
|
|
1655
|
+
"fr": "Exige une preuve de propriété de l'adresse e-mail avant qu'une connexion sociale ne puisse être liée à un compte. Recommandé.",
|
|
1656
|
+
"en": "Require verified email ownership before social sign-in can link to an account. Recommended.",
|
|
1657
|
+
"sw": "Inahitaji uthibitishaji wa umiliki wa barua pepe kabla ya kuingia kwa mitandao kuunganishwa na akaunti. Inapendekezwa."
|
|
1658
|
+
},
|
|
1659
|
+
"Auth.EMAIL_VERIFICATION_TOGGLE": {
|
|
1660
|
+
"de": "E-Mail-Verifizierung erforderlich",
|
|
1661
|
+
"fr": "Vérification de l'e-mail requise",
|
|
1662
|
+
"en": "Require email verification",
|
|
1663
|
+
"sw": "Hitaji uthibitishaji wa barua pepe"
|
|
1664
|
+
},
|
|
1665
|
+
"Auth.EMAIL_VERIFICATION_SAVE_SUCCESS": {
|
|
1666
|
+
"de": "Anforderung zur E-Mail-Verifizierung gespeichert.",
|
|
1667
|
+
"fr": "Exigence de vérification de l'e-mail enregistrée.",
|
|
1668
|
+
"en": "Email verification requirement saved.",
|
|
1669
|
+
"sw": "Mahitaji ya uthibitishaji wa barua pepe yamehifadhiwa."
|
|
1670
|
+
},
|
|
1671
|
+
"Auth.ACCESS_CODE_SINGLE_USE_TITLE": {
|
|
1672
|
+
"de": "Zugangscode: Einmalverwendung",
|
|
1673
|
+
"fr": "Code d'accès : usage unique",
|
|
1674
|
+
"en": "Access Code Single-Use",
|
|
1675
|
+
"sw": "Msimbo wa Ufikiaji: Matumizi ya Mara Moja"
|
|
1676
|
+
},
|
|
1677
|
+
"Auth.ACCESS_CODE_SINGLE_USE_HINT": {
|
|
1678
|
+
"de": "Wenn aktiviert, kann jeder Zugangscode nur einmal eingelöst werden. Empfohlen.",
|
|
1679
|
+
"fr": "Lorsqu'activé, chaque code d'accès ne peut être utilisé qu'une seule fois. Recommandé.",
|
|
1680
|
+
"en": "When enabled, each access code can be redeemed only once. Recommended.",
|
|
1681
|
+
"sw": "Ikiwa imewashwa, kila msimbo wa ufikiaji unaweza kutumika mara moja tu. Inapendekezwa."
|
|
1682
|
+
},
|
|
1683
|
+
"Auth.ACCESS_CODE_SINGLE_USE_TOGGLE": {
|
|
1684
|
+
"de": "Codes sind einmalig verwendbar",
|
|
1685
|
+
"fr": "Codes à usage unique",
|
|
1686
|
+
"en": "Codes are single-use",
|
|
1687
|
+
"sw": "Misimbo ni ya matumizi ya mara moja"
|
|
1688
|
+
},
|
|
1689
|
+
"Auth.ACCESS_CODE_SINGLE_USE_SAVE_SUCCESS": {
|
|
1690
|
+
"de": "Zugangscode-Richtlinie gespeichert.",
|
|
1691
|
+
"fr": "Politique de code d'accès enregistrée.",
|
|
1692
|
+
"en": "Access code policy saved.",
|
|
1693
|
+
"sw": "Sera ya msimbo wa ufikiaji imehifadhiwa."
|
|
1646
1694
|
}
|
|
1647
1695
|
};
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,8 @@ export { UserInviteComponent } from './components/UserInviteComponent';
|
|
|
24
24
|
export { BulkInviteCsvTab } from './components/BulkInviteCsvTab';
|
|
25
25
|
export { RegistrationMethodsManager } from './components/RegistrationMethodsManager';
|
|
26
26
|
export { AuthFactorRequirementCard } from './components/AuthFactorRequirementCard';
|
|
27
|
+
export { EmailVerificationRequirementCard } from './components/EmailVerificationRequirementCard';
|
|
28
|
+
export { AccessCodeSingleUseToggle } from './components/AccessCodeSingleUseToggle';
|
|
27
29
|
export { QrSignupManager } from './components/QrSignupManager';
|
|
28
30
|
// --- 6. Translations ---
|
|
29
31
|
export { authTranslations } from './i18n/authTranslations';
|
|
@@ -16,6 +16,8 @@ import { AccessCodeManager } from '../components/AccessCodeManager';
|
|
|
16
16
|
import { AllowedEmailDomainsManager } from '../components/AllowedEmailDomainsManager';
|
|
17
17
|
import { RegistrationMethodsManager } from '../components/RegistrationMethodsManager';
|
|
18
18
|
import { AuthFactorRequirementCard } from '../components/AuthFactorRequirementCard';
|
|
19
|
+
import { EmailVerificationRequirementCard } from '../components/EmailVerificationRequirementCard';
|
|
20
|
+
import { AccessCodeSingleUseToggle } from '../components/AccessCodeSingleUseToggle';
|
|
19
21
|
import { QrSignupManager } from '../components/QrSignupManager';
|
|
20
22
|
import { QrSignupValidityManager } from '../components/QrSignupValidityManager';
|
|
21
23
|
import { SupportRecoveryRequestsTab } from '../components/SupportRecoveryRequestsTab';
|
|
@@ -129,5 +131,5 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
129
131
|
const activeExtraTab = builtInTabValues.has(safeTab)
|
|
130
132
|
? null
|
|
131
133
|
: extraTabs.find((tab) => tab.value === safeTab);
|
|
132
|
-
return (_jsxs(WidePage, { title: t('Account.TITLE', 'Account & Administration'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('Account.PAGE_TITLE', 'Account'), " \u2013 ", user.email] }) }), _jsx(Tabs, { value: safeTab, onChange: handleTabChange, variant: "scrollable", scrollButtons: "auto", sx: { mb: 3, borderBottom: 1, borderColor: 'divider' }, children: tabs.map((tab) => (_jsx(Tab, { label: tab.label, value: tab.value }, tab.value))) }), safeTab === 'profile' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(ProfileComponent, { onSubmit: handleProfileSubmit, showName: true, showPrivacy: true, showCookies: true }) })), safeTab === 'security' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SecurityComponent, { fromRecovery: fromRecovery, fromWeakLogin: fromWeakLogin }) })), safeTab === 'users' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(UserListComponent, { roles: activeRoles, currentUser: user, extraColumns: userListExtraColumns, extraRowActions: userListExtraRowActions, extraContext: userListExtraContext, refreshTrigger: userListRefreshTrigger, canEditUser: userListCanEditUser, showNewColumn: userListShowNewColumn, showSuccessfulLoginColumn: userListShowSuccessfulLoginColumn, showRoleColumn: userListShowRoleColumn, onChangeRole: userListOnChangeRole, showDeleteAction: userListShowDeleteAction, canDeleteUser: userListCanDeleteUser, onDeleteUser: userListOnDeleteUser }) })), safeTab === 'invite' && (_jsx(Box, { sx: { mt: 2 }, children: _jsxs(Stack, { spacing: 2.5, children: [canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(AuthFactorRequirementCard, { canEdit: canWriteAuthPolicy, policy: authPolicy }) })), canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(RegistrationMethodsManager, { policy: authPolicy, error: authPolicyError, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canSendInvites && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(UserInviteComponent, {}) })), canManageAccessCodes && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_access_code) && (_jsxs(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.ACCESS_CODE_MANAGER_TITLE', 'Access Codes') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Account.ACCESS_CODES_HINT', 'Manage access codes for self-registration.') }), _jsx(AccessCodeManager, {})] })), canSendInvites && showBulkInviteCsvTab && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(BulkInviteCsvTab, Object.assign({}, bulkInviteCsvProps)) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(AllowedEmailDomainsManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain), domains: (authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allowed_email_domains) || [], onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(QrSignupValidityManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canManageSignupQr && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(QrSignupManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days }) }))] }) })), safeTab === 'support' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SupportRecoveryRequestsTab, {}) })), activeExtraTab && (_jsx(Box, { sx: { mt: 2 }, children: (_a = activeExtraTab.render) === null || _a === void 0 ? void 0 : _a.call(activeExtraTab, { user, perms, isSuperUser, t }) }))] }));
|
|
134
|
+
return (_jsxs(WidePage, { title: t('Account.TITLE', 'Account & Administration'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('Account.PAGE_TITLE', 'Account'), " \u2013 ", user.email] }) }), _jsx(Tabs, { value: safeTab, onChange: handleTabChange, variant: "scrollable", scrollButtons: "auto", sx: { mb: 3, borderBottom: 1, borderColor: 'divider' }, children: tabs.map((tab) => (_jsx(Tab, { label: tab.label, value: tab.value }, tab.value))) }), safeTab === 'profile' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(ProfileComponent, { onSubmit: handleProfileSubmit, showName: true, showPrivacy: true, showCookies: true }) })), safeTab === 'security' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SecurityComponent, { fromRecovery: fromRecovery, fromWeakLogin: fromWeakLogin }) })), safeTab === 'users' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(UserListComponent, { roles: activeRoles, currentUser: user, extraColumns: userListExtraColumns, extraRowActions: userListExtraRowActions, extraContext: userListExtraContext, refreshTrigger: userListRefreshTrigger, canEditUser: userListCanEditUser, showNewColumn: userListShowNewColumn, showSuccessfulLoginColumn: userListShowSuccessfulLoginColumn, showRoleColumn: userListShowRoleColumn, onChangeRole: userListOnChangeRole, showDeleteAction: userListShowDeleteAction, canDeleteUser: userListCanDeleteUser, onDeleteUser: userListOnDeleteUser }) })), safeTab === 'invite' && (_jsx(Box, { sx: { mt: 2 }, children: _jsxs(Stack, { spacing: 2.5, children: [canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(AuthFactorRequirementCard, { canEdit: canWriteAuthPolicy, policy: authPolicy }) })), canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(EmailVerificationRequirementCard, { canEdit: canWriteAuthPolicy, policy: authPolicy, onPolicyChange: setAuthPolicy }) })), canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(RegistrationMethodsManager, { policy: authPolicy, error: authPolicyError, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canSendInvites && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(UserInviteComponent, {}) })), canManageAccessCodes && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_access_code) && (_jsxs(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.ACCESS_CODE_MANAGER_TITLE', 'Access Codes') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Account.ACCESS_CODES_HINT', 'Manage access codes for self-registration.') }), _jsx(AccessCodeManager, {})] })), canManageAccessCodes && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_access_code) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(AccessCodeSingleUseToggle, { canEdit: canWriteAuthPolicy, policy: authPolicy, onPolicyChange: setAuthPolicy }) })), canSendInvites && showBulkInviteCsvTab && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(BulkInviteCsvTab, Object.assign({}, bulkInviteCsvProps)) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(AllowedEmailDomainsManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain), domains: (authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allowed_email_domains) || [], onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(QrSignupValidityManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canManageSignupQr && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(QrSignupManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days }) }))] }) })), safeTab === 'support' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SupportRecoveryRequestsTab, {}) })), activeExtraTab && (_jsx(Box, { sx: { mt: 2 }, children: (_a = activeExtraTab.render) === null || _a === void 0 ? void 0 : _a.call(activeExtraTab, { user, perms, isSuperUser, t }) }))] }));
|
|
133
135
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@micha.bigler/ui-core-micha",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.js",
|
|
6
6
|
"repository": {
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
"build": "tsc -p tsconfig.build.json"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
+
"@mui/icons-material": "^7.3.11",
|
|
27
28
|
"typescript": "^5.9.3"
|
|
28
29
|
},
|
|
29
30
|
"dependencies": {
|
|
30
31
|
"react-i18next": "^16.3.5"
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
|
-
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Alert,
|
|
4
|
+
Box,
|
|
5
|
+
FormControlLabel,
|
|
6
|
+
Switch,
|
|
7
|
+
Typography,
|
|
8
|
+
} from '@mui/material';
|
|
9
|
+
import { useTranslation } from 'react-i18next';
|
|
10
|
+
import { fetchAuthPolicy, updateAuthPolicy } from '../auth/authApi';
|
|
11
|
+
|
|
12
|
+
export function AccessCodeSingleUseToggle({ canEdit = true, policy = null, onPolicyChange = null }) {
|
|
13
|
+
const { t } = useTranslation();
|
|
14
|
+
const [value, setValue] = useState(false);
|
|
15
|
+
const [busy, setBusy] = useState(false);
|
|
16
|
+
const [error, setError] = useState('');
|
|
17
|
+
const [success, setSuccess] = useState('');
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (policy) {
|
|
21
|
+
setValue(Boolean(policy?.access_code_single_use));
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
let active = true;
|
|
25
|
+
(async () => {
|
|
26
|
+
try {
|
|
27
|
+
const data = await fetchAuthPolicy();
|
|
28
|
+
if (active) {
|
|
29
|
+
setValue(Boolean(data?.access_code_single_use));
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// Keep defaults when policy is unavailable.
|
|
33
|
+
}
|
|
34
|
+
})();
|
|
35
|
+
return () => {
|
|
36
|
+
active = false;
|
|
37
|
+
};
|
|
38
|
+
}, [policy]);
|
|
39
|
+
|
|
40
|
+
const handleChange = async (event) => {
|
|
41
|
+
if (!canEdit) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const nextValue = event.target.checked;
|
|
45
|
+
const previous = value;
|
|
46
|
+
setValue(nextValue);
|
|
47
|
+
setBusy(true);
|
|
48
|
+
setError('');
|
|
49
|
+
setSuccess('');
|
|
50
|
+
try {
|
|
51
|
+
const updated = await updateAuthPolicy({ access_code_single_use: nextValue });
|
|
52
|
+
if (onPolicyChange) {
|
|
53
|
+
onPolicyChange((current) => ({ ...(current || {}), ...(updated || {}) }));
|
|
54
|
+
}
|
|
55
|
+
setSuccess(t('Auth.ACCESS_CODE_SINGLE_USE_SAVE_SUCCESS', 'Access code policy saved.'));
|
|
56
|
+
} catch (err) {
|
|
57
|
+
setValue(previous);
|
|
58
|
+
setError(t(err?.code || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save access code policy.'));
|
|
59
|
+
} finally {
|
|
60
|
+
setBusy(false);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Box>
|
|
66
|
+
<Typography variant="h6" gutterBottom>
|
|
67
|
+
{t('Auth.ACCESS_CODE_SINGLE_USE_TITLE', 'Access Code Single-Use')}
|
|
68
|
+
</Typography>
|
|
69
|
+
<Typography variant="body2" sx={{ mb: 2, color: 'text.secondary' }}>
|
|
70
|
+
{t(
|
|
71
|
+
'Auth.ACCESS_CODE_SINGLE_USE_HINT',
|
|
72
|
+
'When enabled, each access code can be redeemed only once. Recommended.',
|
|
73
|
+
)}
|
|
74
|
+
</Typography>
|
|
75
|
+
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
|
76
|
+
{success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
|
|
77
|
+
<FormControlLabel
|
|
78
|
+
control={
|
|
79
|
+
<Switch
|
|
80
|
+
checked={value}
|
|
81
|
+
disabled={busy || !canEdit}
|
|
82
|
+
onChange={handleChange}
|
|
83
|
+
/>
|
|
84
|
+
}
|
|
85
|
+
label={t('Auth.ACCESS_CODE_SINGLE_USE_TOGGLE', 'Codes are single-use')}
|
|
86
|
+
/>
|
|
87
|
+
</Box>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Alert,
|
|
4
|
+
Box,
|
|
5
|
+
FormControlLabel,
|
|
6
|
+
Switch,
|
|
7
|
+
Typography,
|
|
8
|
+
} from '@mui/material';
|
|
9
|
+
import { useTranslation } from 'react-i18next';
|
|
10
|
+
import { fetchAuthPolicy, updateAuthPolicy } from '../auth/authApi';
|
|
11
|
+
|
|
12
|
+
export function EmailVerificationRequirementCard({ canEdit = true, policy = null, onPolicyChange = null }) {
|
|
13
|
+
const { t } = useTranslation();
|
|
14
|
+
const [value, setValue] = useState(false);
|
|
15
|
+
const [busy, setBusy] = useState(false);
|
|
16
|
+
const [error, setError] = useState('');
|
|
17
|
+
const [success, setSuccess] = useState('');
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (policy) {
|
|
21
|
+
setValue(Boolean(policy?.require_email_verification));
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
let active = true;
|
|
25
|
+
(async () => {
|
|
26
|
+
try {
|
|
27
|
+
const data = await fetchAuthPolicy();
|
|
28
|
+
if (active) {
|
|
29
|
+
setValue(Boolean(data?.require_email_verification));
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// Keep defaults when policy is unavailable.
|
|
33
|
+
}
|
|
34
|
+
})();
|
|
35
|
+
return () => {
|
|
36
|
+
active = false;
|
|
37
|
+
};
|
|
38
|
+
}, [policy]);
|
|
39
|
+
|
|
40
|
+
const handleChange = async (event) => {
|
|
41
|
+
if (!canEdit) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const nextValue = event.target.checked;
|
|
45
|
+
const previous = value;
|
|
46
|
+
setValue(nextValue);
|
|
47
|
+
setBusy(true);
|
|
48
|
+
setError('');
|
|
49
|
+
setSuccess('');
|
|
50
|
+
try {
|
|
51
|
+
const updated = await updateAuthPolicy({ require_email_verification: nextValue });
|
|
52
|
+
if (onPolicyChange) {
|
|
53
|
+
onPolicyChange((current) => ({ ...(current || {}), ...(updated || {}) }));
|
|
54
|
+
}
|
|
55
|
+
setSuccess(t('Auth.EMAIL_VERIFICATION_SAVE_SUCCESS', 'Email verification requirement saved.'));
|
|
56
|
+
} catch (err) {
|
|
57
|
+
setValue(previous);
|
|
58
|
+
setError(t(err?.code || 'Auth.AUTH_POLICY_UPDATE_FAILED', 'Could not save email verification requirement.'));
|
|
59
|
+
} finally {
|
|
60
|
+
setBusy(false);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Box>
|
|
66
|
+
<Typography variant="h6" gutterBottom>
|
|
67
|
+
{t('Auth.EMAIL_VERIFICATION_TITLE', 'Email Verification')}
|
|
68
|
+
</Typography>
|
|
69
|
+
<Typography variant="body2" sx={{ mb: 2, color: 'text.secondary' }}>
|
|
70
|
+
{t(
|
|
71
|
+
'Auth.EMAIL_VERIFICATION_HINT',
|
|
72
|
+
'Require verified email ownership before social sign-in can link to an account. Recommended.',
|
|
73
|
+
)}
|
|
74
|
+
</Typography>
|
|
75
|
+
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
|
76
|
+
{success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
|
|
77
|
+
<FormControlLabel
|
|
78
|
+
control={
|
|
79
|
+
<Switch
|
|
80
|
+
checked={value}
|
|
81
|
+
disabled={busy || !canEdit}
|
|
82
|
+
onChange={handleChange}
|
|
83
|
+
/>
|
|
84
|
+
}
|
|
85
|
+
label={t('Auth.EMAIL_VERIFICATION_TOGGLE', 'Require email verification')}
|
|
86
|
+
/>
|
|
87
|
+
</Box>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -1691,5 +1691,53 @@ export const authTranslations = {
|
|
|
1691
1691
|
"fr": "Chargement…",
|
|
1692
1692
|
"en": "Loading…",
|
|
1693
1693
|
"sw": "Inapakia..."
|
|
1694
|
+
},
|
|
1695
|
+
"Auth.EMAIL_VERIFICATION_TITLE": {
|
|
1696
|
+
"de": "E-Mail-Verifizierung",
|
|
1697
|
+
"fr": "Vérification de l'e-mail",
|
|
1698
|
+
"en": "Email Verification",
|
|
1699
|
+
"sw": "Uthibitishaji wa Barua Pepe"
|
|
1700
|
+
},
|
|
1701
|
+
"Auth.EMAIL_VERIFICATION_HINT": {
|
|
1702
|
+
"de": "Verlangt einen Nachweis des E-Mail-Besitzes, bevor ein Social-Login mit einem Konto verknüpft werden kann. Empfohlen.",
|
|
1703
|
+
"fr": "Exige une preuve de propriété de l'adresse e-mail avant qu'une connexion sociale ne puisse être liée à un compte. Recommandé.",
|
|
1704
|
+
"en": "Require verified email ownership before social sign-in can link to an account. Recommended.",
|
|
1705
|
+
"sw": "Inahitaji uthibitishaji wa umiliki wa barua pepe kabla ya kuingia kwa mitandao kuunganishwa na akaunti. Inapendekezwa."
|
|
1706
|
+
},
|
|
1707
|
+
"Auth.EMAIL_VERIFICATION_TOGGLE": {
|
|
1708
|
+
"de": "E-Mail-Verifizierung erforderlich",
|
|
1709
|
+
"fr": "Vérification de l'e-mail requise",
|
|
1710
|
+
"en": "Require email verification",
|
|
1711
|
+
"sw": "Hitaji uthibitishaji wa barua pepe"
|
|
1712
|
+
},
|
|
1713
|
+
"Auth.EMAIL_VERIFICATION_SAVE_SUCCESS": {
|
|
1714
|
+
"de": "Anforderung zur E-Mail-Verifizierung gespeichert.",
|
|
1715
|
+
"fr": "Exigence de vérification de l'e-mail enregistrée.",
|
|
1716
|
+
"en": "Email verification requirement saved.",
|
|
1717
|
+
"sw": "Mahitaji ya uthibitishaji wa barua pepe yamehifadhiwa."
|
|
1718
|
+
},
|
|
1719
|
+
"Auth.ACCESS_CODE_SINGLE_USE_TITLE": {
|
|
1720
|
+
"de": "Zugangscode: Einmalverwendung",
|
|
1721
|
+
"fr": "Code d'accès : usage unique",
|
|
1722
|
+
"en": "Access Code Single-Use",
|
|
1723
|
+
"sw": "Msimbo wa Ufikiaji: Matumizi ya Mara Moja"
|
|
1724
|
+
},
|
|
1725
|
+
"Auth.ACCESS_CODE_SINGLE_USE_HINT": {
|
|
1726
|
+
"de": "Wenn aktiviert, kann jeder Zugangscode nur einmal eingelöst werden. Empfohlen.",
|
|
1727
|
+
"fr": "Lorsqu'activé, chaque code d'accès ne peut être utilisé qu'une seule fois. Recommandé.",
|
|
1728
|
+
"en": "When enabled, each access code can be redeemed only once. Recommended.",
|
|
1729
|
+
"sw": "Ikiwa imewashwa, kila msimbo wa ufikiaji unaweza kutumika mara moja tu. Inapendekezwa."
|
|
1730
|
+
},
|
|
1731
|
+
"Auth.ACCESS_CODE_SINGLE_USE_TOGGLE": {
|
|
1732
|
+
"de": "Codes sind einmalig verwendbar",
|
|
1733
|
+
"fr": "Codes à usage unique",
|
|
1734
|
+
"en": "Codes are single-use",
|
|
1735
|
+
"sw": "Misimbo ni ya matumizi ya mara moja"
|
|
1736
|
+
},
|
|
1737
|
+
"Auth.ACCESS_CODE_SINGLE_USE_SAVE_SUCCESS": {
|
|
1738
|
+
"de": "Zugangscode-Richtlinie gespeichert.",
|
|
1739
|
+
"fr": "Politique de code d'accès enregistrée.",
|
|
1740
|
+
"en": "Access code policy saved.",
|
|
1741
|
+
"sw": "Sera ya msimbo wa ufikiaji imehifadhiwa."
|
|
1694
1742
|
}
|
|
1695
1743
|
};
|
package/src/index.js
CHANGED
|
@@ -35,6 +35,8 @@ export { UserInviteComponent } from './components/UserInviteComponent';
|
|
|
35
35
|
export { BulkInviteCsvTab } from './components/BulkInviteCsvTab';
|
|
36
36
|
export { RegistrationMethodsManager } from './components/RegistrationMethodsManager';
|
|
37
37
|
export { AuthFactorRequirementCard } from './components/AuthFactorRequirementCard';
|
|
38
|
+
export { EmailVerificationRequirementCard } from './components/EmailVerificationRequirementCard';
|
|
39
|
+
export { AccessCodeSingleUseToggle } from './components/AccessCodeSingleUseToggle';
|
|
38
40
|
export { QrSignupManager } from './components/QrSignupManager';
|
|
39
41
|
|
|
40
42
|
// --- 6. Translations ---
|
|
@@ -26,6 +26,8 @@ import { AccessCodeManager } from '../components/AccessCodeManager';
|
|
|
26
26
|
import { AllowedEmailDomainsManager } from '../components/AllowedEmailDomainsManager';
|
|
27
27
|
import { RegistrationMethodsManager } from '../components/RegistrationMethodsManager';
|
|
28
28
|
import { AuthFactorRequirementCard } from '../components/AuthFactorRequirementCard';
|
|
29
|
+
import { EmailVerificationRequirementCard } from '../components/EmailVerificationRequirementCard';
|
|
30
|
+
import { AccessCodeSingleUseToggle } from '../components/AccessCodeSingleUseToggle';
|
|
29
31
|
import { QrSignupManager } from '../components/QrSignupManager';
|
|
30
32
|
import { QrSignupValidityManager } from '../components/QrSignupValidityManager';
|
|
31
33
|
import { SupportRecoveryRequestsTab } from '../components/SupportRecoveryRequestsTab';
|
|
@@ -254,6 +256,16 @@ export function AccountPage({
|
|
|
254
256
|
</Paper>
|
|
255
257
|
)}
|
|
256
258
|
|
|
259
|
+
{canViewAuthPolicy && (
|
|
260
|
+
<Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
|
|
261
|
+
<EmailVerificationRequirementCard
|
|
262
|
+
canEdit={canWriteAuthPolicy}
|
|
263
|
+
policy={authPolicy}
|
|
264
|
+
onPolicyChange={setAuthPolicy}
|
|
265
|
+
/>
|
|
266
|
+
</Paper>
|
|
267
|
+
)}
|
|
268
|
+
|
|
257
269
|
{canViewAuthPolicy && (
|
|
258
270
|
<Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
|
|
259
271
|
<RegistrationMethodsManager
|
|
@@ -283,6 +295,16 @@ export function AccountPage({
|
|
|
283
295
|
</Paper>
|
|
284
296
|
)}
|
|
285
297
|
|
|
298
|
+
{canManageAccessCodes && Boolean(authPolicy?.allow_self_signup_access_code) && (
|
|
299
|
+
<Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
|
|
300
|
+
<AccessCodeSingleUseToggle
|
|
301
|
+
canEdit={canWriteAuthPolicy}
|
|
302
|
+
policy={authPolicy}
|
|
303
|
+
onPolicyChange={setAuthPolicy}
|
|
304
|
+
/>
|
|
305
|
+
</Paper>
|
|
306
|
+
)}
|
|
307
|
+
|
|
286
308
|
{canSendInvites && showBulkInviteCsvTab && Boolean(authPolicy?.allow_admin_invite) && (
|
|
287
309
|
<Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
|
|
288
310
|
<BulkInviteCsvTab {...bulkInviteCsvProps} />
|