diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index cfb7e3eb..98122bff 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -405,7 +405,8 @@ "checkingDatabase": "Checking database connection...", "actions": "Actions", "remove": "Remove", - "revoke": "Revoke" + "revoke": "Revoke", + "create": "Create" }, "nav": { "home": "Home", @@ -1872,6 +1873,8 @@ "securityWarningMessage": "Sharing credentials gives the user full access to perform any operations on the server, including changing passwords and deleting files. Only share with trusted users.", "tempUserRecommended": "We recommend enabling 'Create Temporary User' for better security.", "roleManagement": "Role Management", + "manageRoles": "Manage Roles", + "manageRolesFor": "Manage roles for {{username}}", "assignRole": "Assign Role", "removeRole": "Remove Role", "userRoles": "User Roles", @@ -1883,6 +1886,7 @@ "roleRemovedSuccessfully": "Role removed from {{username}} successfully", "failedToRemoveRole": "Failed to remove role", "cannotRemoveSystemRole": "Cannot remove system role", + "cannotShareWithSelf": "Cannot share host with yourself", "auditLogs": "Audit Logs", "viewAuditLogs": "View Audit Logs", "action": "Action", diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index a9f6f84a..69b6cb17 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -385,7 +385,8 @@ "checkingDatabase": "正在检查数据库连接...", "actions": "操作", "remove": "移除", - "revoke": "撤销" + "revoke": "撤销", + "create": "创建" }, "nav": { "home": "首页", @@ -1733,6 +1734,8 @@ "securityWarningMessage": "分享凭据会让用户完全访问服务器并执行任何操作,包括更改密码和删除文件。仅与受信任的用户共享。", "tempUserRecommended": "我们建议启用'创建临时用户'以获得更好的安全性。", "roleManagement": "角色管理", + "manageRoles": "管理角色", + "manageRolesFor": "管理 {{username}} 的角色", "assignRole": "分配角色", "removeRole": "移除角色", "userRoles": "用户角色", @@ -1744,6 +1747,7 @@ "roleRemovedSuccessfully": "已成功从{{username}}移除角色", "failedToRemoveRole": "移除角色失败", "cannotRemoveSystemRole": "无法移除系统角色", + "cannotShareWithSelf": "不能与自己共享主机", "auditLogs": "审计日志", "viewAuditLogs": "查看审计日志", "action": "操作", diff --git a/src/types/index.ts b/src/types/index.ts index 83dad26a..3f5844b5 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -48,6 +48,11 @@ export interface SSHHost { terminalConfig?: TerminalConfig; createdAt: string; updatedAt: string; + + // Shared access metadata + isShared?: boolean; + permissionLevel?: "view" | "manage"; + sharedExpiresAt?: string; } export interface JumpHostData { diff --git a/src/ui/desktop/admin/AdminSettings.tsx b/src/ui/desktop/admin/AdminSettings.tsx index 30955663..e7f38a78 100644 --- a/src/ui/desktop/admin/AdminSettings.tsx +++ b/src/ui/desktop/admin/AdminSettings.tsx @@ -42,6 +42,7 @@ import { Smartphone, Globe, Clock, + UserCog, } from "lucide-react"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; @@ -66,6 +67,12 @@ import { revokeAllUserSessions, linkOIDCToPasswordAccount, unlinkOIDCFromPasswordAccount, + getUserRoles, + assignRoleToUser, + removeRoleFromUser, + getRoles, + type UserRole, + type Role, } from "@/ui/main-axios.ts"; import { RoleManagement } from "./RoleManagement.tsx"; @@ -120,6 +127,16 @@ export function AdminSettings({ null, ); + // Role management states + const [rolesDialogOpen, setRolesDialogOpen] = React.useState(false); + const [selectedUser, setSelectedUser] = React.useState<{ + id: string; + username: 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<{ id: string; @@ -268,6 +285,65 @@ 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); + } + }; + + 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 handleRemoveRole = async (roleId: number) => { + if (!selectedUser) return; + + // Prevent removal of system roles + const roleToRemove = userRoles.find((r) => r.roleId === roleId); + if (roleToRemove && (roleToRemove.roleName === "admin" || roleToRemove.roleName === "user")) { + toast.error(t("rbac.cannotRemoveSystemRole")); + return; + } + + 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 handleToggleRegistration = async (checked: boolean) => { setRegLoading(true); try { @@ -1090,13 +1166,13 @@ export function AdminSettings({ - + {t("admin.username")} - + {t("admin.type")} - + {t("admin.actions")} @@ -1104,7 +1180,7 @@ export function AdminSettings({ {users.map((user) => ( - + {user.username} {user.is_admin && ( @@ -1112,14 +1188,14 @@ export function AdminSettings({ )} - + {user.is_oidc && user.password_hash ? "Dual Auth" : user.is_oidc ? t("admin.external") : t("admin.local")} - +
{user.is_oidc && !user.password_hash && ( )} + + )} +
+ ))} + + )} + + + {/* Assign New Role */} +
+ +
+ {availableRoles + .filter( + (role) => + !role.isSystem && + !userRoles.some((ur) => ur.roleId === role.id), + ) + .map((role) => ( + + ))} +
+
+ + )} + + + + + + ); } diff --git a/src/ui/desktop/admin/RoleManagement.tsx b/src/ui/desktop/admin/RoleManagement.tsx index b129b418..ab53de6f 100644 --- a/src/ui/desktop/admin/RoleManagement.tsx +++ b/src/ui/desktop/admin/RoleManagement.tsx @@ -298,21 +298,23 @@ export function RoleManagement(): React.ReactElement {
- {!role.isSystem && ( - + <> + + + )}
@@ -339,7 +341,7 @@ export function RoleManagement(): React.ReactElement { {/* Create/Edit Role Dialog */} - + {editingRole ? t("rbac.editRole") : t("rbac.createRole")} @@ -351,7 +353,7 @@ export function RoleManagement(): React.ReactElement { -
+
{!editingRole && (
@@ -403,13 +405,13 @@ export function RoleManagement(): React.ReactElement { {/* Assign Role Dialog */} - + {t("rbac.assignRoles")} {t("rbac.assignRolesDescription")} -
+
{/* User Selection */}
diff --git a/src/ui/desktop/apps/host-manager/HostManagerEditor.tsx b/src/ui/desktop/apps/host-manager/HostManagerEditor.tsx index d84c2226..68d3b209 100644 --- a/src/ui/desktop/apps/host-manager/HostManagerEditor.tsx +++ b/src/ui/desktop/apps/host-manager/HostManagerEditor.tsx @@ -1183,9 +1183,11 @@ export function HostManagerEditor({ {t("hosts.statistics")} - - {t("rbac.sharing")} - + {!editingHost?.isShared && ( + + {t("rbac.sharing")} + + )} @@ -3316,13 +3318,15 @@ export function HostManagerEditor({
- + {!(editingHost?.permissionLevel === "view") && ( + + )}
diff --git a/src/ui/desktop/apps/host-manager/HostSharingTab.tsx b/src/ui/desktop/apps/host-manager/HostSharingTab.tsx index 74cdef66..ebbf1d59 100644 --- a/src/ui/desktop/apps/host-manager/HostSharingTab.tsx +++ b/src/ui/desktop/apps/host-manager/HostSharingTab.tsx @@ -35,6 +35,7 @@ import { useConfirmation } from "@/hooks/use-confirmation.ts"; import { getRoles, getUserList, + getUserInfo, shareHost, getHostAccess, revokeHostAccess, @@ -55,7 +56,6 @@ interface User { const PERMISSION_LEVELS = [ { value: "view", labelKey: "rbac.view" }, - { value: "use", labelKey: "rbac.use" }, { value: "manage", labelKey: "rbac.manage" }, ]; @@ -71,13 +71,14 @@ export function HostSharingTab({ const [selectedRoleId, setSelectedRoleId] = React.useState( null, ); - const [permissionLevel, setPermissionLevel] = React.useState("use"); + const [permissionLevel, setPermissionLevel] = React.useState("view"); const [expiresInHours, setExpiresInHours] = React.useState(""); const [roles, setRoles] = React.useState([]); const [users, setUsers] = React.useState([]); const [accessList, setAccessList] = React.useState([]); const [loading, setLoading] = React.useState(false); + const [currentUserId, setCurrentUserId] = React.useState(""); // Load roles const loadRoles = React.useCallback(async () => { @@ -131,6 +132,19 @@ export function HostSharingTab({ } }, [loadRoles, loadUsers, loadAccessList, isNewHost]); + // Load current user ID + React.useEffect(() => { + const fetchCurrentUser = async () => { + try { + const userInfo = await getUserInfo(); + setCurrentUserId(userInfo.userId); + } catch (error) { + console.error("Failed to load current user:", error); + } + }; + fetchCurrentUser(); + }, []); + // Share host const handleShare = async () => { if (!hostId) { @@ -148,6 +162,12 @@ export function HostSharingTab({ return; } + // Prevent sharing with self + if (shareType === "user" && selectedUserId === currentUserId) { + toast.error(t("rbac.cannotShareWithSelf")); + return; + } + try { await shareHost(hostId, { targetType: shareType, @@ -298,13 +318,18 @@ export function HostSharingTab({ {/* Expiration */}
setExpiresInHours(e.target.value)} + onChange={(e) => { + const value = e.target.value; + if (value === "" || /^\d+$/.test(value)) { + setExpiresInHours(value); + } + }} placeholder={t("rbac.neverExpires")} min="1" />