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:
Anthony Stirling 2025-12-11 13:56:35 +00:00 committed by GitHub
parent eb3e57577c
commit 5f072f87bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 331 additions and 54 deletions

View File

@ -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"

View File

@ -69,7 +69,7 @@ const AppConfigModalInner: React.FC<AppConfigModalProps> = ({ 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;

View File

@ -3,6 +3,7 @@ export const VALID_NAV_KEYS = [
'preferences',
'notifications',
'connections',
'account',
'general',
'people',
'teams',

View File

@ -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<void> {
const formData = new FormData();
formData.append('currentPasswordChangeUsername', currentPassword);
formData.append('newUsername', newUsername);
await apiClient.post('/api/v1/user/change-username', formData);
},
};

View File

@ -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: <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)
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: <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)
if (isAdmin || !loginEnabled) {
const requiresLogin = !loginEnabled;

View File

@ -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;

View File

@ -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;