mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Account change details (#5190)
## Summary Accounts setting page to change a users password or username Fix huge bug were users can see admin settings due to hard code admin=true ## Testing - not run (not requested) ------ [Codex Task](https://chatgpt.com/codex/tasks/task_b_6934b8ecdbf08328a0951b46db77dfd2)
This commit is contained in:
@@ -435,6 +435,24 @@ latestVersion = "Latest Version"
|
|||||||
checkForUpdates = "Check for Updates"
|
checkForUpdates = "Check for Updates"
|
||||||
viewDetails = "View Details"
|
viewDetails = "View Details"
|
||||||
|
|
||||||
|
[settings.security]
|
||||||
|
title = "Security"
|
||||||
|
description = "Update your password to keep your account secure."
|
||||||
|
|
||||||
|
[settings.security.password]
|
||||||
|
subtitle = "Change your password. You will be logged out after updating."
|
||||||
|
required = "All fields are required."
|
||||||
|
mismatch = "New passwords do not match."
|
||||||
|
error = "Unable to update password. Please verify your current password and try again."
|
||||||
|
success = "Password updated successfully. Please sign in again."
|
||||||
|
current = "Current password"
|
||||||
|
currentPlaceholder = "Enter your current password"
|
||||||
|
new = "New password"
|
||||||
|
newPlaceholder = "Enter a new password"
|
||||||
|
confirm = "Confirm new password"
|
||||||
|
confirmPlaceholder = "Re-enter your new password"
|
||||||
|
update = "Update password"
|
||||||
|
|
||||||
[settings.hotkeys]
|
[settings.hotkeys]
|
||||||
title = "Keyboard Shortcuts"
|
title = "Keyboard Shortcuts"
|
||||||
description = "Customize keyboard shortcuts for quick tool access. Click \"Change shortcut\" and press a new key combination. Press Esc to cancel."
|
description = "Customize keyboard shortcuts for quick tool access. Click \"Change shortcut\" and press a new key combination. Press Esc to cancel."
|
||||||
@@ -493,6 +511,10 @@ oldPassword = "Current Password"
|
|||||||
newPassword = "New Password"
|
newPassword = "New Password"
|
||||||
confirmNewPassword = "Confirm New Password"
|
confirmNewPassword = "Confirm New Password"
|
||||||
submit = "Submit Changes"
|
submit = "Submit Changes"
|
||||||
|
credsUpdated = "Account updated"
|
||||||
|
description = "Changes saved. Please log in again."
|
||||||
|
error = "Unable to update username. Please verify your password and try again."
|
||||||
|
changeUsername = "Update your username. You will be logged out after updating."
|
||||||
|
|
||||||
[account]
|
[account]
|
||||||
title = "Account Settings"
|
title = "Account Settings"
|
||||||
@@ -5070,6 +5092,7 @@ loading = "Loading..."
|
|||||||
back = "Back"
|
back = "Back"
|
||||||
continue = "Continue"
|
continue = "Continue"
|
||||||
error = "Error"
|
error = "Error"
|
||||||
|
save = "Save"
|
||||||
|
|
||||||
[config.overview]
|
[config.overview]
|
||||||
title = "Application Configuration"
|
title = "Application Configuration"
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const AppConfigModalInner: React.FC<AppConfigModalProps> = ({ opened, onClose })
|
|||||||
}), []);
|
}), []);
|
||||||
|
|
||||||
// Get isAdmin and runningEE from app config
|
// Get isAdmin and runningEE from app config
|
||||||
const isAdmin = true // config?.isAdmin ?? false;
|
const isAdmin = config?.isAdmin ?? false;
|
||||||
const runningEE = config?.runningEE ?? false;
|
const runningEE = config?.runningEE ?? false;
|
||||||
const loginEnabled = config?.enableLogin ?? false;
|
const loginEnabled = config?.enableLogin ?? false;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export const VALID_NAV_KEYS = [
|
|||||||
'preferences',
|
'preferences',
|
||||||
'notifications',
|
'notifications',
|
||||||
'connections',
|
'connections',
|
||||||
|
'account',
|
||||||
'general',
|
'general',
|
||||||
'people',
|
'people',
|
||||||
'teams',
|
'teams',
|
||||||
|
|||||||
@@ -56,4 +56,14 @@ export const accountService = {
|
|||||||
formData.append('newPassword', newPassword);
|
formData.append('newPassword', newPassword);
|
||||||
await apiClient.post('/api/v1/user/change-password-on-login', formData);
|
await apiClient.post('/api/v1/user/change-password-on-login', formData);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change username
|
||||||
|
*/
|
||||||
|
async changeUsername(newUsername: string, currentPassword: string): Promise<void> {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('currentPasswordChangeUsername', currentPassword);
|
||||||
|
formData.append('newUsername', newUsername);
|
||||||
|
await apiClient.post('/api/v1/user/change-username', formData);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import AdminEndpointsSection from '@app/components/shared/config/configSections/
|
|||||||
import AdminAuditSection from '@app/components/shared/config/configSections/AdminAuditSection';
|
import AdminAuditSection from '@app/components/shared/config/configSections/AdminAuditSection';
|
||||||
import AdminUsageSection from '@app/components/shared/config/configSections/AdminUsageSection';
|
import AdminUsageSection from '@app/components/shared/config/configSections/AdminUsageSection';
|
||||||
import ApiKeys from '@app/components/shared/config/configSections/ApiKeys';
|
import ApiKeys from '@app/components/shared/config/configSections/ApiKeys';
|
||||||
|
import AccountSection from '@app/components/shared/config/configSections/AccountSection';
|
||||||
|
import GeneralSection from '@app/components/shared/config/configSections/GeneralSection';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook version of proprietary config nav sections with proper i18n support
|
* Hook version of proprietary config nav sections with proper i18n support
|
||||||
@@ -30,6 +32,23 @@ export const useConfigNavSections = (
|
|||||||
// Get the core sections (just Preferences)
|
// Get the core sections (just Preferences)
|
||||||
const sections = useCoreConfigNavSections(isAdmin, runningEE, loginEnabled);
|
const sections = useCoreConfigNavSections(isAdmin, runningEE, loginEnabled);
|
||||||
|
|
||||||
|
// Add account management under Preferences
|
||||||
|
const preferencesSection = sections.find((section) => section.items.some((item) => item.key === 'general'));
|
||||||
|
if (preferencesSection) {
|
||||||
|
preferencesSection.items = preferencesSection.items.map((item) =>
|
||||||
|
item.key === 'general' ? { ...item, component: <GeneralSection /> } : item
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loginEnabled) {
|
||||||
|
preferencesSection.items.push({
|
||||||
|
key: 'account',
|
||||||
|
label: t('account.accountSettings', 'Account'),
|
||||||
|
icon: 'person-rounded',
|
||||||
|
component: <AccountSection />
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add Admin sections if user is admin OR if login is disabled (but mark as disabled)
|
// Add Admin sections if user is admin OR if login is disabled (but mark as disabled)
|
||||||
if (isAdmin || !loginEnabled) {
|
if (isAdmin || !loginEnabled) {
|
||||||
const requiresLogin = !loginEnabled;
|
const requiresLogin = !loginEnabled;
|
||||||
@@ -220,6 +239,23 @@ export const createConfigNavSections = (
|
|||||||
// Get the core sections (just Preferences)
|
// Get the core sections (just Preferences)
|
||||||
const sections = createCoreConfigNavSections(isAdmin, runningEE, loginEnabled);
|
const sections = createCoreConfigNavSections(isAdmin, runningEE, loginEnabled);
|
||||||
|
|
||||||
|
// Add account management under Preferences
|
||||||
|
const preferencesSection = sections.find((section) => section.items.some((item) => item.key === 'general'));
|
||||||
|
if (preferencesSection) {
|
||||||
|
preferencesSection.items = preferencesSection.items.map((item) =>
|
||||||
|
item.key === 'general' ? { ...item, component: <GeneralSection /> } : item
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loginEnabled) {
|
||||||
|
preferencesSection.items.push({
|
||||||
|
key: 'account',
|
||||||
|
label: 'Account',
|
||||||
|
icon: 'person-rounded',
|
||||||
|
component: <AccountSection />
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add Admin sections if user is admin OR if login is disabled (but mark as disabled)
|
// Add Admin sections if user is admin OR if login is disabled (but mark as disabled)
|
||||||
if (isAdmin || !loginEnabled) {
|
if (isAdmin || !loginEnabled) {
|
||||||
const requiresLogin = !loginEnabled;
|
const requiresLogin = !loginEnabled;
|
||||||
|
|||||||
@@ -0,0 +1,260 @@
|
|||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { Alert, Button, Group, Modal, Paper, PasswordInput, Stack, Text, TextInput } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||||
|
import { alert as showToast } from '@app/components/toast';
|
||||||
|
import { useAuth } from '@app/auth/UseSession';
|
||||||
|
import { accountService } from '@app/services/accountService';
|
||||||
|
import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex';
|
||||||
|
|
||||||
|
const AccountSection: React.FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { user, signOut } = useAuth();
|
||||||
|
const [passwordModalOpen, setPasswordModalOpen] = useState(false);
|
||||||
|
const [usernameModalOpen, setUsernameModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const [currentPassword, setCurrentPassword] = useState('');
|
||||||
|
const [newPassword, setNewPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
const [passwordError, setPasswordError] = useState('');
|
||||||
|
const [passwordSubmitting, setPasswordSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const [currentPasswordForUsername, setCurrentPasswordForUsername] = useState('');
|
||||||
|
const [newUsername, setNewUsername] = useState('');
|
||||||
|
const [usernameError, setUsernameError] = useState('');
|
||||||
|
const [usernameSubmitting, setUsernameSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const userIdentifier = useMemo(() => user?.email || user?.username || '', [user?.email, user?.username]);
|
||||||
|
|
||||||
|
const redirectToLogin = useCallback(() => {
|
||||||
|
window.location.assign('/login');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLogout = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
await signOut();
|
||||||
|
} finally {
|
||||||
|
redirectToLogin();
|
||||||
|
}
|
||||||
|
}, [redirectToLogin, signOut]);
|
||||||
|
|
||||||
|
const handlePasswordSubmit = async (event: React.FormEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!currentPassword || !newPassword || !confirmPassword) {
|
||||||
|
setPasswordError(t('settings.security.password.required', 'All fields are required.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
setPasswordError(t('settings.security.password.mismatch', 'New passwords do not match.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setPasswordSubmitting(true);
|
||||||
|
setPasswordError('');
|
||||||
|
|
||||||
|
await accountService.changePassword(currentPassword, newPassword);
|
||||||
|
|
||||||
|
showToast({
|
||||||
|
alertType: 'success',
|
||||||
|
title: t('settings.security.password.success', 'Password updated successfully. Please sign in again.'),
|
||||||
|
});
|
||||||
|
|
||||||
|
setCurrentPassword('');
|
||||||
|
setNewPassword('');
|
||||||
|
setConfirmPassword('');
|
||||||
|
setPasswordModalOpen(false);
|
||||||
|
await handleLogout();
|
||||||
|
} catch (err) {
|
||||||
|
const axiosError = err as { response?: { data?: { message?: string } } };
|
||||||
|
setPasswordError(
|
||||||
|
axiosError.response?.data?.message ||
|
||||||
|
t('settings.security.password.error', 'Unable to update password. Please verify your current password and try again.')
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setPasswordSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUsernameSubmit = async (event: React.FormEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!currentPasswordForUsername || !newUsername) {
|
||||||
|
setUsernameError(t('settings.security.password.required', 'All fields are required.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setUsernameSubmitting(true);
|
||||||
|
setUsernameError('');
|
||||||
|
|
||||||
|
await accountService.changeUsername(newUsername, currentPasswordForUsername);
|
||||||
|
|
||||||
|
showToast({
|
||||||
|
alertType: 'success',
|
||||||
|
title: t('changeCreds.credsUpdated', 'Account updated'),
|
||||||
|
body: t('changeCreds.description', 'Changes saved. Please log in again.'),
|
||||||
|
});
|
||||||
|
|
||||||
|
setNewUsername('');
|
||||||
|
setCurrentPasswordForUsername('');
|
||||||
|
setUsernameModalOpen(false);
|
||||||
|
await handleLogout();
|
||||||
|
} catch (err) {
|
||||||
|
const axiosError = err as { response?: { data?: { message?: string } } };
|
||||||
|
setUsernameError(
|
||||||
|
axiosError.response?.data?.message ||
|
||||||
|
t('changeCreds.error', 'Unable to update username. Please verify your password and try again.')
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setUsernameSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="md">
|
||||||
|
<div>
|
||||||
|
<Text fw={600} size="lg">
|
||||||
|
{t('account.accountSettings', 'Account')}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{t('changeCreds.header', 'Update Your Account Details')}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Paper withBorder p="md" radius="md">
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{userIdentifier
|
||||||
|
? t('settings.general.user', 'User') + ': ' + userIdentifier
|
||||||
|
: t('account.accountSettings', 'Account Settings')}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Group gap="sm" wrap="wrap">
|
||||||
|
<Button leftSection={<LocalIcon icon="key-rounded" />} onClick={() => setPasswordModalOpen(true)}>
|
||||||
|
{t('settings.security.password.update', 'Update password')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
leftSection={<LocalIcon icon="edit-rounded" />}
|
||||||
|
onClick={() => setUsernameModalOpen(true)}
|
||||||
|
>
|
||||||
|
{t('account.changeUsername', 'Change username')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button variant="outline" color="red" leftSection={<LocalIcon icon="logout-rounded" />} onClick={handleLogout}>
|
||||||
|
{t('settings.general.logout', 'Log out')}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
opened={passwordModalOpen}
|
||||||
|
onClose={() => setPasswordModalOpen(false)}
|
||||||
|
title={t('settings.security.title', 'Change password')}
|
||||||
|
withinPortal
|
||||||
|
zIndex={Z_INDEX_OVER_CONFIG_MODAL}
|
||||||
|
>
|
||||||
|
<form onSubmit={handlePasswordSubmit}>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{t('settings.security.password.subtitle', 'Change your password. You will be logged out after updating.')}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{passwordError && (
|
||||||
|
<Alert icon={<LocalIcon icon="error-rounded" width="1rem" height="1rem" />} color="red" variant="light">
|
||||||
|
{passwordError}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PasswordInput
|
||||||
|
label={t('settings.security.password.current', 'Current password')}
|
||||||
|
placeholder={t('settings.security.password.currentPlaceholder', 'Enter your current password')}
|
||||||
|
value={currentPassword}
|
||||||
|
onChange={(event) => setCurrentPassword(event.currentTarget.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PasswordInput
|
||||||
|
label={t('settings.security.password.new', 'New password')}
|
||||||
|
placeholder={t('settings.security.password.newPlaceholder', 'Enter a new password')}
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(event) => setNewPassword(event.currentTarget.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PasswordInput
|
||||||
|
label={t('settings.security.password.confirm', 'Confirm new password')}
|
||||||
|
placeholder={t('settings.security.password.confirmPlaceholder', 'Re-enter your new password')}
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(event) => setConfirmPassword(event.currentTarget.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Group justify="flex-end" gap="sm">
|
||||||
|
<Button variant="default" onClick={() => setPasswordModalOpen(false)}>
|
||||||
|
{t('common.cancel', 'Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" loading={passwordSubmitting} leftSection={<LocalIcon icon="save-rounded" />}>
|
||||||
|
{t('settings.security.password.update', 'Update password')}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
opened={usernameModalOpen}
|
||||||
|
onClose={() => setUsernameModalOpen(false)}
|
||||||
|
title={t('account.changeUsername', 'Change username')}
|
||||||
|
withinPortal
|
||||||
|
zIndex={Z_INDEX_OVER_CONFIG_MODAL}
|
||||||
|
>
|
||||||
|
<form onSubmit={handleUsernameSubmit}>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{t('changeCreds.changeUsername', 'Update your username. You will be logged out after updating.')}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{usernameError && (
|
||||||
|
<Alert icon={<LocalIcon icon="error-rounded" width="1rem" height="1rem" />} color="red" variant="light">
|
||||||
|
{usernameError}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label={t('changeCreds.newUsername', 'New Username')}
|
||||||
|
placeholder={t('changeCreds.newUsername', 'New Username')}
|
||||||
|
value={newUsername}
|
||||||
|
onChange={(event) => setNewUsername(event.currentTarget.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PasswordInput
|
||||||
|
label={t('changeCreds.oldPassword', 'Current Password')}
|
||||||
|
placeholder={t('changeCreds.oldPassword', 'Current Password')}
|
||||||
|
value={currentPasswordForUsername}
|
||||||
|
onChange={(event) => setCurrentPasswordForUsername(event.currentTarget.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Group justify="flex-end" gap="sm">
|
||||||
|
<Button variant="default" onClick={() => setUsernameModalOpen(false)}>
|
||||||
|
{t('common.cancel', 'Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" loading={usernameSubmitting} leftSection={<LocalIcon icon="save-rounded" />}>
|
||||||
|
{t('common.save', 'Save')}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AccountSection;
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Stack, Text, Button } from '@mantine/core';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useAuth } from '@app/auth/UseSession';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import CoreGeneralSection from '@core/components/shared/config/configSections/GeneralSection';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Proprietary extension of GeneralSection that adds account management
|
|
||||||
*/
|
|
||||||
const GeneralSection: React.FC = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { signOut, user } = useAuth();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
try {
|
|
||||||
await signOut();
|
|
||||||
navigate('/login');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Logout error:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack gap="lg">
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
|
||||||
<div>
|
|
||||||
<Text fw={600} size="lg">{t('settings.general.title', 'General')}</Text>
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
{t('settings.general.description', 'Configure general application preferences.')}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{user && (
|
|
||||||
<Stack gap="xs" align="flex-end">
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
{t('settings.general.user', 'User')}: <strong>{user.email || user.username}</strong>
|
|
||||||
</Text>
|
|
||||||
<Button color="red" variant="outline" size="xs" onClick={handleLogout}>
|
|
||||||
{t('settings.general.logout', 'Log out')}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Render core general section preferences (without title since we show it above) */}
|
|
||||||
<CoreGeneralSection hideTitle />
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GeneralSection;
|
|
||||||
Reference in New Issue
Block a user