Unlock account (#5984)

This commit is contained in:
Anthony Stirling
2026-03-30 16:07:57 +01:00
committed by GitHub
parent 1e97a32d4b
commit 82a3b8c770
10 changed files with 335 additions and 35 deletions

View File

@@ -7978,6 +7978,7 @@ activeSession = "Active session"
addMembers = "Add Members"
admin = "Admin"
confirmDelete = "Are you sure you want to delete this user? This action cannot be undone."
confirmUnlock = "Are you sure you want to unlock this user account?"
deleteUser = "Delete User"
deleteUserError = "Failed to delete user"
deleteUserSuccess = "User deleted successfully"
@@ -7986,6 +7987,8 @@ disable = "Disable"
disabled = "Disabled"
editRole = "Edit Role"
enable = "Enable"
locked = "locked"
lockedBadge = "Locked"
loading = "Loading people..."
loginRequired = "Enable login mode first"
member = "Member"
@@ -7995,6 +7998,9 @@ searchMembers = "Search members..."
status = "Status"
team = "Team"
title = "People"
unlockAccount = "Unlock Account"
unlockUserError = "Failed to unlock user account"
unlockUserSuccess = "User account unlocked successfully"
user = "User"
[workspace.people.actions]

View File

@@ -51,6 +51,7 @@ export default function PeopleSection() {
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [processing, setProcessing] = useState(false);
const [mailEnabled, setMailEnabled] = useState(false);
const [lockedUsers, setLockedUsers] = useState<string[]>([]);
// License information
const [licenseInfo, setLicenseInfo] = useState<{
@@ -80,6 +81,7 @@ export default function PeopleSection() {
: null;
const isCurrentUser = (user: User) => currentUser?.username === user.username;
const isLockedUser = (user: User) => lockedUsers.includes(user.username);
// Form state for edit user modal
const [editForm, setEditForm] = useState({
@@ -128,6 +130,7 @@ export default function PeopleSection() {
totalUsers: adminData.totalUsers,
});
setMailEnabled(adminData.mailEnabled);
setLockedUsers(adminData.lockedUsers || []);
} else {
// Provide example data when login is disabled
const exampleUsers: User[] = [
@@ -189,6 +192,7 @@ export default function PeopleSection() {
setUsers(exampleUsers);
setTeams(exampleTeams);
setMailEnabled(false);
setLockedUsers([]);
// Example license information
setLicenseInfo({
@@ -268,6 +272,26 @@ export default function PeopleSection() {
}
};
const handleUnlockUser = async (user: User) => {
const confirmMessage = t('workspace.people.confirmUnlock', 'Are you sure you want to unlock this user account?');
if (!window.confirm(`${confirmMessage}\n\nUser: ${user.username}`)) {
return;
}
try {
await userManagementService.unlockUser(user.username);
alert({ alertType: 'success', title: t('workspace.people.unlockUserSuccess', 'User account unlocked successfully') });
fetchData();
} catch (error: any) {
console.error('[PeopleSection] Failed to unlock user:', error);
const errorMessage = error.response?.data?.message ||
error.response?.data?.error ||
error.message ||
t('workspace.people.unlockUserError', 'Failed to unlock user account');
alert({ alertType: 'error', title: errorMessage });
}
};
const openEditModal = (user: User) => {
setSelectedUser(user);
setEditForm({
@@ -389,6 +413,12 @@ export default function PeopleSection() {
</Text>
)}
{lockedUsers.length > 0 && (
<Badge color="orange" variant="light" size="sm">
{lockedUsers.length} {t('workspace.people.locked', 'locked')}
</Badge>
)}
{licenseInfo.premiumEnabled && licenseInfo.licenseMaxUsers > 0 && (
<Badge color="blue" variant="light" size="sm">
+{licenseInfo.licenseMaxUsers} {t('workspace.people.license.fromLicense', 'from license')}
@@ -497,22 +527,29 @@ export default function PeopleSection() {
</Avatar>
</Tooltip>
<Box style={{ minWidth: 0, flex: 1 }}>
<Tooltip label={user.username} disabled={user.username.length <= 20} zIndex={Z_INDEX_OVER_CONFIG_MODAL}>
<Text
size="sm"
fw={500}
maw={200}
style={{
lineHeight: 1.3,
opacity: user.enabled ? 1 : 0.6,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{user.username}
</Text>
</Tooltip>
<Group gap={6} wrap="nowrap" align="center">
<Tooltip label={user.username} disabled={user.username.length <= 20} zIndex={Z_INDEX_OVER_CONFIG_MODAL}>
<Text
size="sm"
fw={500}
maw={200}
style={{
lineHeight: 1.3,
opacity: user.enabled ? 1 : 0.6,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{user.username}
</Text>
</Tooltip>
{isLockedUser(user) && (
<Badge color="orange" variant="light" size="xs">
{t('workspace.people.lockedBadge', 'Locked')}
</Badge>
)}
</Group>
{user.email && (
<Text size="xs" c="dimmed" truncate style={{ lineHeight: 1.3 }}>
{user.email}
@@ -612,6 +649,15 @@ export default function PeopleSection() {
{user.enabled ? t('workspace.people.disable') : t('workspace.people.enable')}
</Menu.Item>
)}
{!isCurrentUser(user) && isLockedUser(user) && (
<Menu.Item
leftSection={<LocalIcon icon="lock-open" width="1rem" height="1rem" />}
onClick={() => handleUnlockUser(user)}
disabled={!loginEnabled}
>
{t('workspace.people.unlockAccount', 'Unlock Account')}
</Menu.Item>
)}
{!isCurrentUser(user) && user.mfaEnabled && (
<>
<Menu.Divider />

View File

@@ -51,6 +51,9 @@ export default function TeamDetailsSection({ teamId, onBack }: TeamDetailsSectio
availableSlots: number;
} | null>(null);
const [mailEnabled, setMailEnabled] = useState(false);
const [lockedUsers, setLockedUsers] = useState<string[]>([]);
const isLockedUser = (user: User) => lockedUsers.includes(user.username);
useEffect(() => {
fetchTeamDetails();
@@ -75,6 +78,7 @@ export default function TeamDetailsSection({ teamId, onBack }: TeamDetailsSectio
availableSlots: adminData.availableSlots,
});
setMailEnabled(adminData.mailEnabled);
setLockedUsers(adminData.lockedUsers || []);
} catch (error) {
console.error('Failed to fetch team details:', error);
alert({ alertType: 'error', title: t('workspace.teams.loadError', 'Failed to load team details') });
@@ -171,6 +175,26 @@ export default function TeamDetailsSection({ teamId, onBack }: TeamDetailsSectio
}
};
const handleUnlockUser = async (user: User) => {
const confirmMessage = t('workspace.people.confirmUnlock', 'Are you sure you want to unlock this user account?');
if (!window.confirm(`${confirmMessage}\n\nUser: ${user.username}`)) {
return;
}
try {
await userManagementService.unlockUser(user.username);
alert({ alertType: 'success', title: t('workspace.people.unlockUserSuccess', 'User account unlocked successfully') });
fetchTeamDetails();
} catch (error: any) {
console.error('[TeamDetailsSection] Failed to unlock user:', error);
const errorMessage = error.response?.data?.message ||
error.response?.data?.error ||
error.message ||
t('workspace.people.unlockUserError', 'Failed to unlock user account');
alert({ alertType: 'error', title: errorMessage });
}
};
const openChangeTeamModal = (user: User) => {
setSelectedUser(user);
setSelectedTeamId(user.team?.id?.toString() || '');
@@ -335,22 +359,29 @@ export default function TeamDetailsSection({ teamId, onBack }: TeamDetailsSectio
</Avatar>
</Tooltip>
<Box style={{ minWidth: 0, flex: 1 }}>
<Tooltip label={user.username} disabled={user.username.length <= 20} zIndex={Z_INDEX_OVER_CONFIG_MODAL}>
<Text
size="sm"
fw={500}
maw={200}
style={{
lineHeight: 1.3,
opacity: user.enabled ? 1 : 0.6,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{user.username}
</Text>
</Tooltip>
<Group gap={6} wrap="nowrap" align="center">
<Tooltip label={user.username} disabled={user.username.length <= 20} zIndex={Z_INDEX_OVER_CONFIG_MODAL}>
<Text
size="sm"
fw={500}
maw={200}
style={{
lineHeight: 1.3,
opacity: user.enabled ? 1 : 0.6,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{user.username}
</Text>
</Tooltip>
{isLockedUser(user) && (
<Badge color="orange" variant="light" size="xs">
{t('workspace.people.lockedBadge', 'Locked')}
</Badge>
)}
</Group>
{user.email && (
<Text size="xs" c="dimmed" truncate style={{ lineHeight: 1.3 }}>
{user.email}
@@ -420,6 +451,15 @@ export default function TeamDetailsSection({ teamId, onBack }: TeamDetailsSectio
>
{t('workspace.people.changePassword.action', 'Change password')}
</Menu.Item>
{isLockedUser(user) && (
<Menu.Item
leftSection={<LocalIcon icon="lock-open" width="1rem" height="1rem" />}
onClick={() => handleUnlockUser(user)}
disabled={processing}
>
{t('workspace.people.unlockAccount', 'Unlock Account')}
</Menu.Item>
)}
{team.name !== 'Internal' && team.name !== 'Default' && (
<Menu.Item
leftSection={<LocalIcon icon="person-remove" width="1rem" height="1rem" />}

View File

@@ -40,6 +40,7 @@ export interface AdminSettingsData {
premiumEnabled: boolean;
mailEnabled: boolean;
userSettings?: Record<string, any>;
lockedUsers?: string[];
}
export interface CreateUserRequest {
@@ -302,6 +303,15 @@ export const userManagementService = {
} as any);
},
/**
* Unlock a locked user account (admin only)
*/
async unlockUser(username: string): Promise<void> {
await apiClient.post(`/api/v1/user/admin/unlockUser/${username}`, null, {
suppressErrorToast: true,
} as any);
},
/**
* Disable MFA for a user (admin only)
*/