diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index 9be190247..83963987d 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -435,6 +435,24 @@ latestVersion = "Latest Version" checkForUpdates = "Check for Updates" 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] title = "Keyboard Shortcuts" 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" confirmNewPassword = "Confirm New Password" 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] title = "Account Settings" @@ -5070,6 +5092,7 @@ loading = "Loading..." back = "Back" continue = "Continue" error = "Error" +save = "Save" [config.overview] title = "Application Configuration" diff --git a/frontend/src/core/components/shared/AppConfigModal.tsx b/frontend/src/core/components/shared/AppConfigModal.tsx index 6dd7491d4..42e6099e3 100644 --- a/frontend/src/core/components/shared/AppConfigModal.tsx +++ b/frontend/src/core/components/shared/AppConfigModal.tsx @@ -69,7 +69,7 @@ const AppConfigModalInner: React.FC = ({ opened, onClose }) }), []); // Get isAdmin and runningEE from app config - const isAdmin = true // config?.isAdmin ?? false; + const isAdmin = config?.isAdmin ?? false; const runningEE = config?.runningEE ?? false; const loginEnabled = config?.enableLogin ?? false; diff --git a/frontend/src/core/components/shared/config/types.ts b/frontend/src/core/components/shared/config/types.ts index d4bd9a83c..740e6130e 100644 --- a/frontend/src/core/components/shared/config/types.ts +++ b/frontend/src/core/components/shared/config/types.ts @@ -3,6 +3,7 @@ export const VALID_NAV_KEYS = [ 'preferences', 'notifications', 'connections', + 'account', 'general', 'people', 'teams', diff --git a/frontend/src/core/services/accountService.ts b/frontend/src/core/services/accountService.ts index 72d9f1873..cc8c2527f 100644 --- a/frontend/src/core/services/accountService.ts +++ b/frontend/src/core/services/accountService.ts @@ -56,4 +56,14 @@ export const accountService = { formData.append('newPassword', newPassword); await apiClient.post('/api/v1/user/change-password-on-login', formData); }, + + /** + * Change username + */ + async changeUsername(newUsername: string, currentPassword: string): Promise { + const formData = new FormData(); + formData.append('currentPasswordChangeUsername', currentPassword); + formData.append('newUsername', newUsername); + await apiClient.post('/api/v1/user/change-username', formData); + }, }; diff --git a/frontend/src/proprietary/components/shared/config/configNavSections.tsx b/frontend/src/proprietary/components/shared/config/configNavSections.tsx index 6a64e5fbc..0073696f9 100644 --- a/frontend/src/proprietary/components/shared/config/configNavSections.tsx +++ b/frontend/src/proprietary/components/shared/config/configNavSections.tsx @@ -16,6 +16,8 @@ import AdminEndpointsSection from '@app/components/shared/config/configSections/ import AdminAuditSection from '@app/components/shared/config/configSections/AdminAuditSection'; import AdminUsageSection from '@app/components/shared/config/configSections/AdminUsageSection'; 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 @@ -30,6 +32,23 @@ export const useConfigNavSections = ( // Get the core sections (just Preferences) 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: } : item + ); + + if (loginEnabled) { + preferencesSection.items.push({ + key: 'account', + label: t('account.accountSettings', 'Account'), + icon: 'person-rounded', + component: + }); + } + } + // Add Admin sections if user is admin OR if login is disabled (but mark as disabled) if (isAdmin || !loginEnabled) { const requiresLogin = !loginEnabled; @@ -220,6 +239,23 @@ export const createConfigNavSections = ( // Get the core sections (just Preferences) 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: } : item + ); + + if (loginEnabled) { + preferencesSection.items.push({ + key: 'account', + label: 'Account', + icon: 'person-rounded', + component: + }); + } + } + // Add Admin sections if user is admin OR if login is disabled (but mark as disabled) if (isAdmin || !loginEnabled) { const requiresLogin = !loginEnabled; diff --git a/frontend/src/proprietary/components/shared/config/configSections/AccountSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AccountSection.tsx new file mode 100644 index 000000000..f627302b4 --- /dev/null +++ b/frontend/src/proprietary/components/shared/config/configSections/AccountSection.tsx @@ -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 ( + +
+ + {t('account.accountSettings', 'Account')} + + + {t('changeCreds.header', 'Update Your Account Details')} + +
+ + + + + {userIdentifier + ? t('settings.general.user', 'User') + ': ' + userIdentifier + : t('account.accountSettings', 'Account Settings')} + + + + + + + + + + + + + setPasswordModalOpen(false)} + title={t('settings.security.title', 'Change password')} + withinPortal + zIndex={Z_INDEX_OVER_CONFIG_MODAL} + > +
+ + + {t('settings.security.password.subtitle', 'Change your password. You will be logged out after updating.')} + + + {passwordError && ( + } color="red" variant="light"> + {passwordError} + + )} + + setCurrentPassword(event.currentTarget.value)} + required + /> + + setNewPassword(event.currentTarget.value)} + required + /> + + setConfirmPassword(event.currentTarget.value)} + required + /> + + + + + + +
+
+ + setUsernameModalOpen(false)} + title={t('account.changeUsername', 'Change username')} + withinPortal + zIndex={Z_INDEX_OVER_CONFIG_MODAL} + > +
+ + + {t('changeCreds.changeUsername', 'Update your username. You will be logged out after updating.')} + + + {usernameError && ( + } color="red" variant="light"> + {usernameError} + + )} + + setNewUsername(event.currentTarget.value)} + required + /> + + setCurrentPasswordForUsername(event.currentTarget.value)} + required + /> + + + + + + +
+
+
+ ); +}; + +export default AccountSection; diff --git a/frontend/src/proprietary/components/shared/config/configSections/GeneralSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/GeneralSection.tsx deleted file mode 100644 index 7894319cb..000000000 --- a/frontend/src/proprietary/components/shared/config/configSections/GeneralSection.tsx +++ /dev/null @@ -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 ( - -
-
- {t('settings.general.title', 'General')} - - {t('settings.general.description', 'Configure general application preferences.')} - -
- - {user && ( - - - {t('settings.general.user', 'User')}: {user.email || user.username} - - - - )} -
- - {/* Render core general section preferences (without title since we show it above) */} - -
- ); -}; - -export default GeneralSection;