mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-12-18 20:04:17 +01: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:
parent
eb3e57577c
commit
5f072f87bb
@ -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"
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ export const VALID_NAV_KEYS = [
|
||||
'preferences',
|
||||
'notifications',
|
||||
'connections',
|
||||
'account',
|
||||
'general',
|
||||
'people',
|
||||
'teams',
|
||||
|
||||
@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
Loading…
Reference in New Issue
Block a user