Add admin password reset option for users (#5180)

## Summary
- add backend support for admins to reset user passwords and optionally
email notifications when SMTP is enabled
- surface mail capability in admin settings data for the UI
- add a shared change-password modal hooked into People and Team user
actions with random password generation and email options

## Testing
- not run (not requested)


------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_6934b978fe3c83289b5b95dec79b3d38)

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
Anthony Stirling
2025-12-10 10:10:40 +00:00
committed by GitHub
co-authored by Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
parent c980ee10c0
commit 9c03914edd
10 changed files with 577 additions and 11 deletions
@@ -30,6 +30,7 @@ import LoginRequiredBanner from '@app/components/shared/config/LoginRequiredBann
import { useNavigate } from 'react-router-dom';
import UpdateSeatsButton from '@app/components/shared/UpdateSeatsButton';
import { useLicense } from '@app/contexts/LicenseContext';
import ChangeUserPasswordModal from '@app/components/shared/ChangeUserPasswordModal';
export default function PeopleSection() {
const { t } = useTranslation();
@@ -43,8 +44,11 @@ export default function PeopleSection() {
const [searchQuery, setSearchQuery] = useState('');
const [inviteModalOpened, setInviteModalOpened] = useState(false);
const [editUserModalOpened, setEditUserModalOpened] = useState(false);
const [changePasswordModalOpened, setChangePasswordModalOpened] = useState(false);
const [passwordUser, setPasswordUser] = useState<User | null>(null);
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [processing, setProcessing] = useState(false);
const [mailEnabled, setMailEnabled] = useState(false);
// License information
const [licenseInfo, setLicenseInfo] = useState<{
@@ -119,6 +123,7 @@ export default function PeopleSection() {
premiumEnabled: adminData.premiumEnabled,
totalUsers: adminData.totalUsers,
});
setMailEnabled(adminData.mailEnabled);
} else {
// Provide example data when login is disabled
const exampleUsers: User[] = [
@@ -179,6 +184,7 @@ export default function PeopleSection() {
setUsers(exampleUsers);
setTeams(exampleTeams);
setMailEnabled(false);
// Example license information
setLicenseInfo({
@@ -267,6 +273,16 @@ export default function PeopleSection() {
setEditUserModalOpened(true);
};
const openChangePasswordModal = (user: User) => {
setPasswordUser(user);
setChangePasswordModalOpened(true);
};
const closeChangePasswordModal = () => {
setChangePasswordModalOpened(false);
setPasswordUser(null);
};
const closeEditModal = () => {
setEditUserModalOpened(false);
setSelectedUser(null);
@@ -562,7 +578,20 @@ export default function PeopleSection() {
</ActionIcon>
</Menu.Target>
<Menu.Dropdown style={{ zIndex: Z_INDEX_OVER_CONFIG_MODAL }}>
<Menu.Item onClick={() => openEditModal(user)} disabled={!loginEnabled}>{t('workspace.people.editRole')}</Menu.Item>
<Menu.Item
leftSection={<LocalIcon icon="edit" width="1rem" height="1rem" />}
onClick={() => openEditModal(user)}
disabled={!loginEnabled}
>
{t('workspace.people.editRole')}
</Menu.Item>
<Menu.Item
leftSection={<LocalIcon icon="lock" width="1rem" height="1rem" />}
onClick={() => openChangePasswordModal(user)}
disabled={!loginEnabled}
>
{t('workspace.people.changePassword.action', 'Change password')}
</Menu.Item>
<Menu.Item
leftSection={user.enabled ? <LocalIcon icon="person-off" width="1rem" height="1rem" /> : <LocalIcon icon="person-check" width="1rem" height="1rem" />}
onClick={() => handleToggleEnabled(user)}
@@ -591,6 +620,14 @@ export default function PeopleSection() {
onSuccess={fetchData}
/>
<ChangeUserPasswordModal
opened={changePasswordModalOpened}
onClose={closeChangePasswordModal}
user={passwordUser}
onSuccess={fetchData}
mailEnabled={mailEnabled}
/>
{/* Edit User Modal */}
<Modal
opened={editUserModalOpened}
@@ -22,6 +22,7 @@ import { alert } from '@app/components/toast';
import { teamService, Team } from '@app/services/teamService';
import { User, userManagementService } from '@app/services/userManagementService';
import { Z_INDEX_OVER_CONFIG_MODAL } from '@app/styles/zIndex';
import ChangeUserPasswordModal from '@app/components/shared/ChangeUserPasswordModal';
interface TeamDetailsSectionProps {
teamId: number;
@@ -38,6 +39,8 @@ export default function TeamDetailsSection({ teamId, onBack }: TeamDetailsSectio
const [userLastRequest, setUserLastRequest] = useState<Record<string, number>>({});
const [addMemberModalOpened, setAddMemberModalOpened] = useState(false);
const [changeTeamModalOpened, setChangeTeamModalOpened] = useState(false);
const [changePasswordModalOpened, setChangePasswordModalOpened] = useState(false);
const [passwordUser, setPasswordUser] = useState<User | null>(null);
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [selectedUserId, setSelectedUserId] = useState<string>('');
const [selectedTeamId, setSelectedTeamId] = useState<string>('');
@@ -47,6 +50,7 @@ export default function TeamDetailsSection({ teamId, onBack }: TeamDetailsSectio
const [licenseInfo, setLicenseInfo] = useState<{
availableSlots: number;
} | null>(null);
const [mailEnabled, setMailEnabled] = useState(false);
useEffect(() => {
fetchTeamDetails();
@@ -70,6 +74,7 @@ export default function TeamDetailsSection({ teamId, onBack }: TeamDetailsSectio
setLicenseInfo({
availableSlots: adminData.availableSlots,
});
setMailEnabled(adminData.mailEnabled);
} catch (error) {
console.error('Failed to fetch team details:', error);
alert({ alertType: 'error', title: t('workspace.teams.loadError', 'Failed to load team details') });
@@ -172,6 +177,16 @@ export default function TeamDetailsSection({ teamId, onBack }: TeamDetailsSectio
setChangeTeamModalOpened(true);
};
const openChangePasswordModal = (user: User) => {
setPasswordUser(user);
setChangePasswordModalOpened(true);
};
const closeChangePasswordModal = () => {
setChangePasswordModalOpened(false);
setPasswordUser(null);
};
const handleChangeTeam = async () => {
if (!selectedUser || !selectedTeamId) {
alert({ alertType: 'error', title: t('workspace.teams.changeTeam.selectTeamRequired', 'Please select a team') });
@@ -398,6 +413,13 @@ export default function TeamDetailsSection({ teamId, onBack }: TeamDetailsSectio
>
{t('workspace.teams.changeTeam.label', 'Change Team')}
</Menu.Item>
<Menu.Item
leftSection={<LocalIcon icon="lock" width="1rem" height="1rem" />}
onClick={() => openChangePasswordModal(user)}
disabled={processing}
>
{t('workspace.people.changePassword.action', 'Change password')}
</Menu.Item>
{team.name !== 'Internal' && team.name !== 'Default' && (
<Menu.Item
leftSection={<LocalIcon icon="person-remove" width="1rem" height="1rem" />}
@@ -427,6 +449,14 @@ export default function TeamDetailsSection({ teamId, onBack }: TeamDetailsSectio
</Table.Tbody>
</Table>
<ChangeUserPasswordModal
opened={changePasswordModalOpened}
onClose={closeChangePasswordModal}
user={passwordUser}
onSuccess={fetchTeamDetails}
mailEnabled={mailEnabled}
/>
{/* Add Member Modal */}
<Modal
opened={addMemberModalOpened}