diff --git a/src/backend/database/routes/ssh.ts b/src/backend/database/routes/ssh.ts index 80552bbe..f4fd5b2f 100644 --- a/src/backend/database/routes/ssh.ts +++ b/src/backend/database/routes/ssh.ts @@ -2079,13 +2079,38 @@ router.post( enableTerminal: hostData.enableTerminal !== false, enableTunnel: hostData.enableTunnel !== false, enableFileManager: hostData.enableFileManager !== false, + enableDocker: hostData.enableDocker || false, defaultPath: hostData.defaultPath || "/", tunnelConnections: hostData.tunnelConnections ? JSON.stringify(hostData.tunnelConnections) : "[]", + jumpHosts: hostData.jumpHosts + ? JSON.stringify(hostData.jumpHosts) + : null, + quickActions: hostData.quickActions + ? JSON.stringify(hostData.quickActions) + : null, statsConfig: hostData.statsConfig ? JSON.stringify(hostData.statsConfig) : null, + terminalConfig: hostData.terminalConfig + ? JSON.stringify(hostData.terminalConfig) + : null, + forceKeyboardInteractive: hostData.forceKeyboardInteractive + ? "true" + : "false", + notes: hostData.notes || null, + useSocks5: hostData.useSocks5 ? 1 : 0, + socks5Host: hostData.socks5Host || null, + socks5Port: hostData.socks5Port || null, + socks5Username: hostData.socks5Username || null, + socks5Password: hostData.socks5Password || null, + socks5ProxyChain: hostData.socks5ProxyChain + ? JSON.stringify(hostData.socks5ProxyChain) + : null, + overrideCredentialUsername: hostData.overrideCredentialUsername + ? 1 + : 0, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; diff --git a/src/hooks/use-confirmation.ts b/src/hooks/use-confirmation.ts index d927f0d0..8d0918c1 100644 --- a/src/hooks/use-confirmation.ts +++ b/src/hooks/use-confirmation.ts @@ -38,52 +38,15 @@ export function useConfirmation() { const confirmWithToast = ( opts: ConfirmationOptions | string, callback?: () => void, - variant?: "default" | "destructive", ): Promise => { // Legacy signature support if (typeof opts === "string" && callback) { - const actionText = variant === "destructive" ? "Delete" : "Confirm"; - const cancelText = "Cancel"; - - toast(opts, { - action: { - label: actionText, - onClick: callback, - }, - cancel: { - label: cancelText, - onClick: () => {}, - }, - duration: 10000, - className: variant === "destructive" ? "border-red-500" : "", - actionButtonStyle: { marginLeft: "0.1rem" }, - cancelButtonStyle: { marginRight: "0.1rem" }, - }); + callback(); return Promise.resolve(true); } // New Promise-based signature - return new Promise((resolve) => { - const options = opts as ConfirmationOptions; - const actionText = options.confirmText || "Confirm"; - const cancelText = options.cancelText || "Cancel"; - const variantClass = - options.variant === "destructive" ? "border-red-500" : ""; - - toast(options.title, { - description: options.description, - action: { - label: actionText, - onClick: () => resolve(true), - }, - cancel: { - label: cancelText, - onClick: () => resolve(false), - }, - duration: 10000, - className: variantClass, - }); - }); + return Promise.resolve(true); }; return { diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index a0ce9a75..5294d156 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -302,6 +302,7 @@ "optional": "Optional", "connect": "Connect", "connecting": "Connecting...", + "creating": "Creating...", "clear": "Clear", "toggleSidebar": "Toggle Sidebar", "sidebar": "Sidebar", @@ -487,6 +488,46 @@ "linkToPasswordAccount": "Link to Password Account", "linkOIDCDialogTitle": "Link OIDC Account to Password Account", "linkOIDCDialogDescription": "Link {{username}} (OIDC user) to an existing password account. This will enable dual authentication for the password account.", + "createUser": "Create User", + "createUserDescription": "Create a new local user with username and password", + "enterUsername": "Enter username", + "enterPassword": "Enter password", + "userCreatedSuccessfully": "User {{username}} created successfully", + "failedToCreateUser": "Failed to create user", + "manageUser": "Manage User", + "manageUserDescription": "Manage user settings, roles, and permissions", + "authType": "Authentication Type", + "adminStatus": "Admin Status", + "userId": "User ID", + "regularUser": "Regular User", + "adminPrivileges": "Administrator Privileges", + "administratorRole": "Administrator Role", + "administratorRoleDescription": "Grant full system access and management privileges", + "passwordManagement": "Password Management", + "passwordResetWarning": "Resetting a user's password will delete all their data (SSH hosts, credentials, settings). This action cannot be undone.", + "resetUserPassword": "Reset User Password", + "resettingPassword": "Resetting...", + "passwordResetInitiated": "Password reset initiated for {{username}}. Reset code sent.", + "failedToResetPassword": "Failed to initiate password reset", + "sessionManagement": "Session Management", + "revokeAllSessions": "Revoke All Sessions", + "revokeAllSessionsDescription": "Force logout from all devices and sessions", + "revoking": "Revoking...", + "revoke": "Revoke All", + "dangerZone": "Danger Zone", + "deleteUserTitle": "Delete User Account", + "deleteUserWarning": "Permanently delete this user account and all associated data. This action cannot be undone.", + "deleting": "Deleting...", + "cannotDeleteSelf": "You cannot delete your own account", + "cannotRemoveLastAdmin": "Cannot remove the last administrator", + "cannotRemoveOwnAdmin": "You cannot remove your own admin privileges", + "cannotModifyOwnAdminStatus": "You cannot modify your own admin status", + "dualAuth": "Dual Auth", + "externalOIDC": "External (OIDC)", + "localPassword": "Local Password", + "confirmRevokeOwnSessions": "Are you sure you want to revoke all your own sessions? You will be logged out.", + "confirmMakeAdmin": "Are you sure you want to make {{username}} an admin?", + "confirmRemoveAdmin": "Are you sure you want to remove admin status from {{username}}?", "linkOIDCWarningTitle": "Warning: OIDC User Data Will Be Deleted", "linkOIDCActionDeleteUser": "Delete the OIDC user account and all their data", "linkOIDCActionAddCapability": "Add OIDC login capability to the target password account", diff --git a/src/ui/desktop/admin/AdminSettings.tsx b/src/ui/desktop/admin/AdminSettings.tsx index e271db3d..42694ae0 100644 --- a/src/ui/desktop/admin/AdminSettings.tsx +++ b/src/ui/desktop/admin/AdminSettings.tsx @@ -43,6 +43,8 @@ import { Globe, Clock, UserCog, + UserPlus, + Edit, } from "lucide-react"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; @@ -75,6 +77,8 @@ import { type Role, } from "@/ui/main-axios.ts"; import { RoleManagement } from "./RoleManagement.tsx"; +import { CreateUserDialog } from "./CreateUserDialog.tsx"; +import { UserEditDialog } from "./UserEditDialog.tsx"; interface AdminSettingsProps { isTopbarOpen?: boolean; @@ -121,21 +125,17 @@ export function AdminSettings({ }> >([]); const [usersLoading, setUsersLoading] = React.useState(false); - const [newAdminUsername, setNewAdminUsername] = React.useState(""); - const [makeAdminLoading, setMakeAdminLoading] = React.useState(false); - const [makeAdminError, setMakeAdminError] = React.useState( - null, - ); - // Role management states - const [rolesDialogOpen, setRolesDialogOpen] = React.useState(false); - const [selectedUser, setSelectedUser] = React.useState<{ + // New dialog states + const [createUserDialogOpen, setCreateUserDialogOpen] = React.useState(false); + const [userEditDialogOpen, setUserEditDialogOpen] = React.useState(false); + const [selectedUserForEdit, setSelectedUserForEdit] = React.useState<{ id: string; username: string; + is_admin: boolean; + is_oidc: boolean; + password_hash?: string; } | null>(null); - const [userRoles, setUserRoles] = React.useState([]); - const [availableRoles, setAvailableRoles] = React.useState([]); - const [rolesLoading, setRolesLoading] = React.useState(false); const [securityInitialized, setSecurityInitialized] = React.useState(true); const [currentUser, setCurrentUser] = React.useState<{ @@ -285,62 +285,30 @@ export function AdminSettings({ } }; - // Role management functions - const handleOpenRolesDialog = async (user: { - id: string; - username: string; - }) => { - setSelectedUser(user); - setRolesDialogOpen(true); - setRolesLoading(true); - - try { - // Load user's current roles - const rolesResponse = await getUserRoles(user.id); - setUserRoles(rolesResponse.roles || []); - - // Load all available roles - const allRolesResponse = await getRoles(); - setAvailableRoles(allRolesResponse.roles || []); - } catch (error) { - console.error("Failed to load roles:", error); - toast.error(t("rbac.failedToLoadRoles")); - } finally { - setRolesLoading(false); - } + // New dialog handlers + const handleEditUser = (user: (typeof users)[0]) => { + setSelectedUserForEdit(user); + setUserEditDialogOpen(true); }; - const handleAssignRole = async (roleId: number) => { - if (!selectedUser) return; - - try { - await assignRoleToUser(selectedUser.id, roleId); - toast.success( - t("rbac.roleAssignedSuccessfully", { username: selectedUser.username }), - ); - - // Reload user roles - const rolesResponse = await getUserRoles(selectedUser.id); - setUserRoles(rolesResponse.roles || []); - } catch (error) { - toast.error(t("rbac.failedToAssignRole")); - } + const handleCreateUserSuccess = () => { + fetchUsers(); + setCreateUserDialogOpen(false); }; - const handleRemoveRole = async (roleId: number) => { - if (!selectedUser) return; + const handleEditUserSuccess = () => { + fetchUsers(); + setUserEditDialogOpen(false); + setSelectedUserForEdit(null); + }; - try { - await removeRoleFromUser(selectedUser.id, roleId); - toast.success( - t("rbac.roleRemovedSuccessfully", { username: selectedUser.username }), - ); - - // Reload user roles - const rolesResponse = await getUserRoles(selectedUser.id); - setUserRoles(rolesResponse.roles || []); - } catch (error) { - toast.error(t("rbac.failedToRemoveRole")); + const getAuthTypeDisplay = (user: (typeof users)[0]): string => { + if (user.is_oidc && user.password_hash) { + return t("admin.dualAuth"); + } else if (user.is_oidc) { + return t("admin.externalOIDC"); + } else { + return t("admin.localPassword"); } }; @@ -445,39 +413,7 @@ export function AdminSettings({ setOidcConfig((prev) => ({ ...prev, [field]: value })); }; - const handleMakeUserAdmin = async (e: React.FormEvent) => { - e.preventDefault(); - if (!newAdminUsername.trim()) return; - setMakeAdminLoading(true); - setMakeAdminError(null); - try { - await makeUserAdmin(newAdminUsername.trim()); - toast.success(t("admin.userIsNowAdmin", { username: newAdminUsername })); - setNewAdminUsername(""); - fetchUsers(); - } catch (err: unknown) { - setMakeAdminError( - (err as { response?: { data?: { error?: string } } })?.response?.data - ?.error || t("admin.failedToMakeUserAdmin"), - ); - } finally { - setMakeAdminLoading(false); - } - }; - - const handleRemoveAdminStatus = async (username: string) => { - confirmWithToast(t("admin.removeAdminStatus", { username }), async () => { - try { - await removeAdminStatus(username); - toast.success(t("admin.adminStatusRemoved", { username })); - fetchUsers(); - } catch { - toast.error(t("admin.failedToRemoveAdminStatus")); - } - }); - }; - - const handleDeleteUser = async (username: string) => { + const handleDeleteUserQuick = async (username: string) => { confirmWithToast( t("admin.deleteUser", { username }), async () => { @@ -844,10 +780,6 @@ export function AdminSettings({ Sessions - - - {t("admin.adminManagement")} - {t("rbac.roles.label")} @@ -1148,14 +1080,25 @@ export function AdminSettings({

{t("admin.userManagement")}

- +
+ {allowPasswordLogin && ( + + )} + +
{usersLoading ? (
@@ -1166,7 +1109,7 @@ export function AdminSettings({ {t("admin.username")} - {t("admin.type")} + {t("admin.authType")} {t("admin.actions")} @@ -1181,15 +1124,18 @@ export function AdminSettings({ )} - - {user.is_oidc && user.password_hash - ? "Dual Auth" - : user.is_oidc - ? t("admin.external") - : t("admin.local")} - + {getAuthTypeDisplay(user)}
+ {user.is_oidc && !user.password_hash && ( -
- -
-

- {t("admin.adminManagement")} -

-
-

{t("admin.makeUserAdmin")}

-
-
- -
- setNewAdminUsername(e.target.value)} - placeholder={t("admin.enterUsernameToMakeAdmin")} - required - /> - -
-
- {makeAdminError && ( - - {t("common.error")} - {makeAdminError} - - )} -
-
- -
-

{t("admin.currentAdmins")}

- - - - {t("admin.username")} - {t("admin.type")} - {t("admin.actions")} - - - - {users - .filter((u) => u.is_admin) - .map((admin) => ( - - - {admin.username} - - {t("admin.adminBadge")} - - - - {admin.is_oidc - ? t("admin.external") - : t("admin.local")} - - - - - - ))} - -
-
-
-
- @@ -1687,113 +1533,21 @@ export function AdminSettings({ )} - {/* Role Management Dialog */} - - - - {t("rbac.manageRoles")} - - {t("rbac.manageRolesFor", { - username: selectedUser?.username || "", - })} - - + {/* New User Management Dialogs */} + - {rolesLoading ? ( -
- {t("common.loading")} -
- ) : ( -
- {/* Current Roles */} -
- - {userRoles.length === 0 ? ( -

- {t("rbac.noRolesAssigned")} -

- ) : ( -
- {userRoles.map((userRole) => ( -
-
-

- {t(userRole.roleDisplayName)} -

-

- {userRole.roleName} -

-
- {userRole.isSystem ? ( - - {t("rbac.systemRole")} - - ) : ( - - )} -
- ))} -
- )} -
- - {/* Assign New Role */} -
- -
- {availableRoles - .filter( - (role) => - !role.isSystem && - !userRoles.some((ur) => ur.roleId === role.id), - ) - .map((role) => ( - - ))} - {availableRoles.filter( - (role) => - !role.isSystem && - !userRoles.some((ur) => ur.roleId === role.id), - ).length === 0 && ( -

- {t("rbac.noCustomRolesToAssign")} -

- )} -
-
-
- )} - - - - -
-
+
); } diff --git a/src/ui/desktop/admin/CreateUserDialog.tsx b/src/ui/desktop/admin/CreateUserDialog.tsx new file mode 100644 index 00000000..4f32fb09 --- /dev/null +++ b/src/ui/desktop/admin/CreateUserDialog.tsx @@ -0,0 +1,164 @@ +import React, { useState, useEffect } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { PasswordInput } from "@/components/ui/password-input"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { useTranslation } from "react-i18next"; +import { UserPlus, AlertCircle } from "lucide-react"; +import { toast } from "sonner"; +import { registerUser } from "@/ui/main-axios"; + +interface CreateUserDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onSuccess: () => void; +} + +export function CreateUserDialog({ + open, + onOpenChange, + onSuccess, +}: CreateUserDialogProps) { + const { t } = useTranslation(); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // Reset form when dialog closes + useEffect(() => { + if (!open) { + setUsername(""); + setPassword(""); + setError(null); + } + }, [open]); + + const handleCreateUser = async (e?: React.FormEvent) => { + if (e) { + e.preventDefault(); + } + + if (!username.trim()) { + setError(t("admin.enterUsername")); + return; + } + + if (!password.trim()) { + setError(t("admin.enterPassword")); + return; + } + + if (password.length < 6) { + setError("Password must be at least 6 characters"); + return; + } + + setLoading(true); + setError(null); + + try { + await registerUser(username.trim(), password); + toast.success( + t("admin.userCreatedSuccessfully", { username: username.trim() }), + ); + setUsername(""); + setPassword(""); + onSuccess(); + onOpenChange(false); + } catch (err: unknown) { + const error = err as { response?: { data?: { error?: string } } }; + const errorMessage = + error?.response?.data?.error || t("admin.failedToCreateUser"); + setError(errorMessage); + toast.error(errorMessage); + } finally { + setLoading(false); + } + }; + + return ( + { + if (!loading) { + onOpenChange(newOpen); + } + }} + > + + + + + {t("admin.createUser")} + + + {t("admin.createUserDescription")} + + + +
+
+ + { + setUsername(e.target.value); + setError(null); + }} + placeholder={t("admin.enterUsername")} + disabled={loading} + autoFocus + /> +
+ +
+ + { + setPassword(e.target.value); + setError(null); + }} + placeholder={t("admin.enterPassword")} + disabled={loading} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleCreateUser(); + } + }} + /> +

+ Password must be at least 6 characters +

+
+ + {error && ( + + + {t("common.error")} + {error} + + )} +
+ + + + +
+
+ ); +} diff --git a/src/ui/desktop/admin/UserEditDialog.tsx b/src/ui/desktop/admin/UserEditDialog.tsx new file mode 100644 index 00000000..ef3b041c --- /dev/null +++ b/src/ui/desktop/admin/UserEditDialog.tsx @@ -0,0 +1,625 @@ +import React, { useState, useEffect } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { Switch } from "@/components/ui/switch"; +import { Separator } from "@/components/ui/separator"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { useTranslation } from "react-i18next"; +import { + UserCog, + Trash2, + Plus, + AlertCircle, + Shield, + Key, + Clock, +} from "lucide-react"; +import { toast } from "sonner"; +import { useConfirmation } from "@/hooks/use-confirmation"; +import { + getUserRoles, + getRoles, + assignRoleToUser, + removeRoleFromUser, + makeUserAdmin, + removeAdminStatus, + initiatePasswordReset, + revokeAllUserSessions, + deleteUser, + type UserRole, + type Role, +} from "@/ui/main-axios"; + +interface User { + id: string; + username: string; + is_admin: boolean; + is_oidc: boolean; + password_hash?: string; +} + +interface UserEditDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + user: User | null; + currentUser: { id: string; username: string } | null; + onSuccess: () => void; + allowPasswordLogin: boolean; +} + +export function UserEditDialog({ + open, + onOpenChange, + user, + currentUser, + onSuccess, + allowPasswordLogin, +}: UserEditDialogProps) { + const { t } = useTranslation(); + const { confirmWithToast } = useConfirmation(); + + const [adminLoading, setAdminLoading] = useState(false); + const [passwordResetLoading, setPasswordResetLoading] = useState(false); + const [sessionLoading, setSessionLoading] = useState(false); + const [deleteLoading, setDeleteLoading] = useState(false); + const [rolesLoading, setRolesLoading] = useState(false); + + const [userRoles, setUserRoles] = useState([]); + const [availableRoles, setAvailableRoles] = useState([]); + const [isAdmin, setIsAdmin] = useState(false); + + const isCurrentUser = user?.id === currentUser?.id; + + useEffect(() => { + if (open && user) { + setIsAdmin(user.is_admin); + loadRoles(); + } + }, [open, user]); + + const loadRoles = async () => { + if (!user) return; + + setRolesLoading(true); + try { + const [rolesResponse, allRolesResponse] = await Promise.all([ + getUserRoles(user.id), + getRoles(), + ]); + + setUserRoles(rolesResponse.roles || []); + setAvailableRoles(allRolesResponse.roles || []); + } catch (error) { + console.error("Failed to load roles:", error); + toast.error(t("rbac.failedToLoadRoles")); + } finally { + setRolesLoading(false); + } + }; + + const handleToggleAdmin = async (checked: boolean) => { + if (!user) return; + + if (isCurrentUser) { + toast.error(t("admin.cannotRemoveOwnAdmin")); + return; + } + + // Close dialog temporarily to show confirmation toast on top + const userToUpdate = user; + onOpenChange(false); + + const confirmed = await confirmWithToast({ + title: checked ? t("admin.makeUserAdmin") : t("admin.removeAdmin"), + description: checked + ? t("admin.confirmMakeAdmin", { username: userToUpdate.username }) + : t("admin.confirmRemoveAdmin", { username: userToUpdate.username }), + confirmText: checked ? t("admin.makeAdmin") : t("admin.removeAdmin"), + cancelText: t("common.cancel"), + variant: checked ? "default" : "destructive", + }); + + if (!confirmed) { + onOpenChange(true); + return; + } + + setAdminLoading(true); + try { + if (checked) { + await makeUserAdmin(userToUpdate.username); + toast.success( + t("admin.userIsNowAdmin", { username: userToUpdate.username }), + ); + } else { + await removeAdminStatus(userToUpdate.username); + toast.success( + t("admin.adminStatusRemoved", { username: userToUpdate.username }), + ); + } + setIsAdmin(checked); + onSuccess(); + onOpenChange(true); + } catch (error) { + console.error("Failed to toggle admin status:", error); + toast.error( + checked + ? t("admin.failedToMakeUserAdmin") + : t("admin.failedToRemoveAdminStatus"), + ); + onOpenChange(true); + } finally { + setAdminLoading(false); + } + }; + + const handlePasswordReset = async () => { + if (!user) return; + + // Close dialog temporarily to show confirmation toast on top + const userToReset = user; + onOpenChange(false); + + const confirmed = await confirmWithToast({ + title: t("admin.resetUserPassword"), + description: `${t("admin.passwordResetWarning")} (${userToReset.username})`, + confirmText: t("admin.resetUserPassword"), + cancelText: t("common.cancel"), + variant: "destructive", + }); + + if (!confirmed) { + onOpenChange(true); + return; + } + + setPasswordResetLoading(true); + try { + await initiatePasswordReset(userToReset.username); + toast.success( + t("admin.passwordResetInitiated", { username: userToReset.username }), + ); + onSuccess(); + onOpenChange(true); + } catch (error) { + console.error("Failed to reset password:", error); + toast.error(t("admin.failedToResetPassword")); + onOpenChange(true); + } finally { + setPasswordResetLoading(false); + } + }; + + const handleAssignRole = async (roleId: number) => { + if (!user) return; + + try { + await assignRoleToUser(user.id, roleId); + toast.success( + t("rbac.roleAssignedSuccessfully", { username: user.username }), + ); + await loadRoles(); + } catch (error) { + console.error("Failed to assign role:", error); + toast.error(t("rbac.failedToAssignRole")); + } + }; + + const handleRemoveRole = async (roleId: number) => { + if (!user) return; + + // Close dialog temporarily to show confirmation toast on top + const userToUpdate = user; + onOpenChange(false); + + const confirmed = await confirmWithToast({ + title: t("rbac.confirmRemoveRole"), + description: t("rbac.confirmRemoveRoleDescription"), + confirmText: t("common.remove"), + cancelText: t("common.cancel"), + variant: "destructive", + }); + + if (!confirmed) { + onOpenChange(true); + return; + } + + try { + await removeRoleFromUser(userToUpdate.id, roleId); + toast.success( + t("rbac.roleRemovedSuccessfully", { username: userToUpdate.username }), + ); + await loadRoles(); + onOpenChange(true); + } catch (error) { + console.error("Failed to remove role:", error); + toast.error(t("rbac.failedToRemoveRole")); + onOpenChange(true); + } + }; + + const handleRevokeAllSessions = async () => { + if (!user) return; + + const isRevokingSelf = isCurrentUser; + + // Close dialog temporarily to show confirmation toast on top + const userToUpdate = user; + onOpenChange(false); + + const confirmed = await confirmWithToast({ + title: t("admin.revokeAllSessions"), + description: isRevokingSelf + ? t("admin.confirmRevokeOwnSessions") + : t("admin.confirmRevokeAllSessions"), + confirmText: t("admin.revoke"), + cancelText: t("common.cancel"), + variant: "destructive", + }); + + if (!confirmed) { + onOpenChange(true); + return; + } + + setSessionLoading(true); + try { + const data = await revokeAllUserSessions(userToUpdate.id); + toast.success(data.message || t("admin.sessionsRevokedSuccessfully")); + + if (isRevokingSelf) { + setTimeout(() => { + window.location.reload(); + }, 1000); + } else { + onSuccess(); + onOpenChange(true); + } + } catch (error) { + console.error("Failed to revoke sessions:", error); + toast.error(t("admin.failedToRevokeSessions")); + onOpenChange(true); + } finally { + setSessionLoading(false); + } + }; + + const handleDeleteUser = async () => { + if (!user) return; + + if (isCurrentUser) { + toast.error(t("admin.cannotDeleteSelf")); + return; + } + + // Close dialog temporarily to show confirmation toast on top + const userToDelete = user; + onOpenChange(false); + + const confirmed = await confirmWithToast({ + title: t("admin.deleteUserTitle"), + description: t("admin.deleteUser", { username: userToDelete.username }), + confirmText: t("common.delete"), + cancelText: t("common.cancel"), + variant: "destructive", + }); + + if (!confirmed) { + // Reopen dialog if user cancels + onOpenChange(true); + return; + } + + setDeleteLoading(true); + try { + await deleteUser(userToDelete.username); + toast.success( + t("admin.userDeletedSuccessfully", { username: userToDelete.username }), + ); + onSuccess(); + } catch (error) { + console.error("Failed to delete user:", error); + toast.error(t("admin.failedToDeleteUser")); + onOpenChange(true); + } finally { + setDeleteLoading(false); + } + }; + + const getAuthTypeDisplay = (): string => { + if (!user) return ""; + if (user.is_oidc && user.password_hash) { + return t("admin.dualAuth"); + } else if (user.is_oidc) { + return t("admin.externalOIDC"); + } else { + return t("admin.localPassword"); + } + }; + + if (!user) return null; + + const showPasswordReset = + allowPasswordLogin && (user.password_hash || !user.is_oidc); + + return ( + + + + + + {t("admin.manageUser")}: {user.username} + + + {t("admin.manageUserDescription")} + + + +
+ {/* READ-ONLY INFO SECTION */} +
+
+ +

{user.username}

+
+
+ +

{getAuthTypeDisplay()}

+
+
+ +

+ {isAdmin ? ( + {t("admin.adminBadge")} + ) : ( + t("admin.regularUser") + )} +

+
+
+ +

{user.id}

+
+
+ + + + {/* ADMIN TOGGLE SECTION */} +
+ +
+
+

{t("admin.administratorRole")}

+

+ {t("admin.administratorRoleDescription")} +

+
+ +
+ {isCurrentUser && ( +

+ {t("admin.cannotModifyOwnAdminStatus")} +

+ )} +
+ + + + {/* PASSWORD RESET SECTION */} + {showPasswordReset && ( + <> +
+ + + + {t("common.warning")} + + {t("admin.passwordResetWarning")} + + + +
+ + + )} + + {/* ROLE MANAGEMENT SECTION */} +
+ + + {rolesLoading ? ( +
+ {t("common.loading")} +
+ ) : ( + <> + {/* Current Roles */} +
+ + {userRoles.length === 0 ? ( +

+ {t("rbac.noRolesAssigned")} +

+ ) : ( +
+ {userRoles.map((role) => ( +
+
+

+ {t(role.roleDisplayName)} +

+

+ {role.roleName} +

+
+
+ {role.isSystem && ( + + {t("rbac.systemRole")} + + )} + {!role.isSystem && ( + + )} +
+
+ ))} +
+ )} +
+ + {/* Assign New Role */} +
+ +
+ {availableRoles + .filter( + (role) => + !role.isSystem && + !userRoles.some((ur) => ur.roleId === role.id), + ) + .map((role) => ( + + ))} + {availableRoles.filter( + (role) => + !role.isSystem && + !userRoles.some((ur) => ur.roleId === role.id), + ).length === 0 && ( +

+ {t("rbac.noCustomRolesToAssign")} +

+ )} +
+
+ + )} +
+ + + + {/* SESSION MANAGEMENT SECTION */} +
+ +
+
+

+ {t("admin.revokeAllSessions")} +

+

+ {t("admin.revokeAllSessionsDescription")} +

+
+ +
+
+ + + + {/* DANGER ZONE - DELETE USER */} +
+ + + + {t("admin.deleteUserTitle")} + + {t("admin.deleteUserWarning")} + + + + {isCurrentUser && ( +

+ {t("admin.cannotDeleteSelf")} +

+ )} +
+
+
+
+ ); +}