Enhance RBAC system with UI improvements and security fixes
- Move role assignment to Users tab with per-user role management - Protect system roles (admin/user) from editing and manual assignment - Simplify permission system: remove Use level, keep View and Manage - Hide Update button and Sharing tab for view-only/shared hosts - Prevent users from sharing hosts with themselves - Unify table and modal styling across admin panels - Auto-assign system roles on user registration - Add permission metadata to host interface
This commit is contained in:
@@ -405,7 +405,8 @@
|
|||||||
"checkingDatabase": "Checking database connection...",
|
"checkingDatabase": "Checking database connection...",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"revoke": "Revoke"
|
"revoke": "Revoke",
|
||||||
|
"create": "Create"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"home": "Home",
|
"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.",
|
"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.",
|
"tempUserRecommended": "We recommend enabling 'Create Temporary User' for better security.",
|
||||||
"roleManagement": "Role Management",
|
"roleManagement": "Role Management",
|
||||||
|
"manageRoles": "Manage Roles",
|
||||||
|
"manageRolesFor": "Manage roles for {{username}}",
|
||||||
"assignRole": "Assign Role",
|
"assignRole": "Assign Role",
|
||||||
"removeRole": "Remove Role",
|
"removeRole": "Remove Role",
|
||||||
"userRoles": "User Roles",
|
"userRoles": "User Roles",
|
||||||
@@ -1883,6 +1886,7 @@
|
|||||||
"roleRemovedSuccessfully": "Role removed from {{username}} successfully",
|
"roleRemovedSuccessfully": "Role removed from {{username}} successfully",
|
||||||
"failedToRemoveRole": "Failed to remove role",
|
"failedToRemoveRole": "Failed to remove role",
|
||||||
"cannotRemoveSystemRole": "Cannot remove system role",
|
"cannotRemoveSystemRole": "Cannot remove system role",
|
||||||
|
"cannotShareWithSelf": "Cannot share host with yourself",
|
||||||
"auditLogs": "Audit Logs",
|
"auditLogs": "Audit Logs",
|
||||||
"viewAuditLogs": "View Audit Logs",
|
"viewAuditLogs": "View Audit Logs",
|
||||||
"action": "Action",
|
"action": "Action",
|
||||||
|
|||||||
@@ -385,7 +385,8 @@
|
|||||||
"checkingDatabase": "正在检查数据库连接...",
|
"checkingDatabase": "正在检查数据库连接...",
|
||||||
"actions": "操作",
|
"actions": "操作",
|
||||||
"remove": "移除",
|
"remove": "移除",
|
||||||
"revoke": "撤销"
|
"revoke": "撤销",
|
||||||
|
"create": "创建"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"home": "首页",
|
"home": "首页",
|
||||||
@@ -1733,6 +1734,8 @@
|
|||||||
"securityWarningMessage": "分享凭据会让用户完全访问服务器并执行任何操作,包括更改密码和删除文件。仅与受信任的用户共享。",
|
"securityWarningMessage": "分享凭据会让用户完全访问服务器并执行任何操作,包括更改密码和删除文件。仅与受信任的用户共享。",
|
||||||
"tempUserRecommended": "我们建议启用'创建临时用户'以获得更好的安全性。",
|
"tempUserRecommended": "我们建议启用'创建临时用户'以获得更好的安全性。",
|
||||||
"roleManagement": "角色管理",
|
"roleManagement": "角色管理",
|
||||||
|
"manageRoles": "管理角色",
|
||||||
|
"manageRolesFor": "管理 {{username}} 的角色",
|
||||||
"assignRole": "分配角色",
|
"assignRole": "分配角色",
|
||||||
"removeRole": "移除角色",
|
"removeRole": "移除角色",
|
||||||
"userRoles": "用户角色",
|
"userRoles": "用户角色",
|
||||||
@@ -1744,6 +1747,7 @@
|
|||||||
"roleRemovedSuccessfully": "已成功从{{username}}移除角色",
|
"roleRemovedSuccessfully": "已成功从{{username}}移除角色",
|
||||||
"failedToRemoveRole": "移除角色失败",
|
"failedToRemoveRole": "移除角色失败",
|
||||||
"cannotRemoveSystemRole": "无法移除系统角色",
|
"cannotRemoveSystemRole": "无法移除系统角色",
|
||||||
|
"cannotShareWithSelf": "不能与自己共享主机",
|
||||||
"auditLogs": "审计日志",
|
"auditLogs": "审计日志",
|
||||||
"viewAuditLogs": "查看审计日志",
|
"viewAuditLogs": "查看审计日志",
|
||||||
"action": "操作",
|
"action": "操作",
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ export interface SSHHost {
|
|||||||
terminalConfig?: TerminalConfig;
|
terminalConfig?: TerminalConfig;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
|
||||||
|
// Shared access metadata
|
||||||
|
isShared?: boolean;
|
||||||
|
permissionLevel?: "view" | "manage";
|
||||||
|
sharedExpiresAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface JumpHostData {
|
export interface JumpHostData {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import {
|
|||||||
Smartphone,
|
Smartphone,
|
||||||
Globe,
|
Globe,
|
||||||
Clock,
|
Clock,
|
||||||
|
UserCog,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -66,6 +67,12 @@ import {
|
|||||||
revokeAllUserSessions,
|
revokeAllUserSessions,
|
||||||
linkOIDCToPasswordAccount,
|
linkOIDCToPasswordAccount,
|
||||||
unlinkOIDCFromPasswordAccount,
|
unlinkOIDCFromPasswordAccount,
|
||||||
|
getUserRoles,
|
||||||
|
assignRoleToUser,
|
||||||
|
removeRoleFromUser,
|
||||||
|
getRoles,
|
||||||
|
type UserRole,
|
||||||
|
type Role,
|
||||||
} from "@/ui/main-axios.ts";
|
} from "@/ui/main-axios.ts";
|
||||||
import { RoleManagement } from "./RoleManagement.tsx";
|
import { RoleManagement } from "./RoleManagement.tsx";
|
||||||
|
|
||||||
@@ -120,6 +127,16 @@ export function AdminSettings({
|
|||||||
null,
|
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<UserRole[]>([]);
|
||||||
|
const [availableRoles, setAvailableRoles] = React.useState<Role[]>([]);
|
||||||
|
const [rolesLoading, setRolesLoading] = React.useState(false);
|
||||||
|
|
||||||
const [securityInitialized, setSecurityInitialized] = React.useState(true);
|
const [securityInitialized, setSecurityInitialized] = React.useState(true);
|
||||||
const [currentUser, setCurrentUser] = React.useState<{
|
const [currentUser, setCurrentUser] = React.useState<{
|
||||||
id: string;
|
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) => {
|
const handleToggleRegistration = async (checked: boolean) => {
|
||||||
setRegLoading(true);
|
setRegLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -1090,13 +1166,13 @@ export function AdminSettings({
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="px-4">
|
<TableHead>
|
||||||
{t("admin.username")}
|
{t("admin.username")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="px-4">
|
<TableHead>
|
||||||
{t("admin.type")}
|
{t("admin.type")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="px-4">
|
<TableHead>
|
||||||
{t("admin.actions")}
|
{t("admin.actions")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -1104,7 +1180,7 @@ export function AdminSettings({
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{users.map((user) => (
|
{users.map((user) => (
|
||||||
<TableRow key={user.id}>
|
<TableRow key={user.id}>
|
||||||
<TableCell className="px-4 font-medium">
|
<TableCell className="font-medium">
|
||||||
{user.username}
|
{user.username}
|
||||||
{user.is_admin && (
|
{user.is_admin && (
|
||||||
<span className="ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-muted/50 text-muted-foreground border border-border">
|
<span className="ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-muted/50 text-muted-foreground border border-border">
|
||||||
@@ -1112,14 +1188,14 @@ export function AdminSettings({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="px-4">
|
<TableCell>
|
||||||
{user.is_oidc && user.password_hash
|
{user.is_oidc && user.password_hash
|
||||||
? "Dual Auth"
|
? "Dual Auth"
|
||||||
: user.is_oidc
|
: user.is_oidc
|
||||||
? t("admin.external")
|
? t("admin.external")
|
||||||
: t("admin.local")}
|
: t("admin.local")}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="px-4">
|
<TableCell>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{user.is_oidc && !user.password_hash && (
|
{user.is_oidc && !user.password_hash && (
|
||||||
<Button
|
<Button
|
||||||
@@ -1150,6 +1226,20 @@ export function AdminSettings({
|
|||||||
<Unlink className="h-4 w-4" />
|
<Unlink className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handleOpenRolesDialog({
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="text-purple-600 hover:text-purple-700 hover:bg-purple-50"
|
||||||
|
title={t("rbac.manageRoles")}
|
||||||
|
>
|
||||||
|
<UserCog className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -1199,12 +1289,12 @@ export function AdminSettings({
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="px-4">Device</TableHead>
|
<TableHead>Device</TableHead>
|
||||||
<TableHead className="px-4">User</TableHead>
|
<TableHead>User</TableHead>
|
||||||
<TableHead className="px-4">Created</TableHead>
|
<TableHead>Created</TableHead>
|
||||||
<TableHead className="px-4">Last Active</TableHead>
|
<TableHead>Last Active</TableHead>
|
||||||
<TableHead className="px-4">Expires</TableHead>
|
<TableHead>Expires</TableHead>
|
||||||
<TableHead className="px-4">
|
<TableHead>
|
||||||
{t("admin.actions")}
|
{t("admin.actions")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -1239,7 +1329,7 @@ export function AdminSettings({
|
|||||||
session.isRevoked ? "opacity-50" : undefined
|
session.isRevoked ? "opacity-50" : undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TableCell className="px-4">
|
<TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<DeviceIcon className="h-4 w-4" />
|
<DeviceIcon className="h-4 w-4" />
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
@@ -1254,19 +1344,19 @@ export function AdminSettings({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="px-4">
|
<TableCell>
|
||||||
{session.username || session.userId}
|
{session.username || session.userId}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="px-4 text-sm text-muted-foreground">
|
<TableCell className="text-sm text-muted-foreground">
|
||||||
{formatDate(createdDate)}
|
{formatDate(createdDate)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="px-4 text-sm text-muted-foreground">
|
<TableCell className="text-sm text-muted-foreground">
|
||||||
{formatDate(lastActiveDate)}
|
{formatDate(lastActiveDate)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="px-4 text-sm text-muted-foreground">
|
<TableCell className="text-sm text-muted-foreground">
|
||||||
{formatDate(expiresDate)}
|
{formatDate(expiresDate)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="px-4">
|
<TableCell>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -1354,13 +1444,13 @@ export function AdminSettings({
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="px-4">
|
<TableHead>
|
||||||
{t("admin.username")}
|
{t("admin.username")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="px-4">
|
<TableHead>
|
||||||
{t("admin.type")}
|
{t("admin.type")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="px-4">
|
<TableHead>
|
||||||
{t("admin.actions")}
|
{t("admin.actions")}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -1370,18 +1460,18 @@ export function AdminSettings({
|
|||||||
.filter((u) => u.is_admin)
|
.filter((u) => u.is_admin)
|
||||||
.map((admin) => (
|
.map((admin) => (
|
||||||
<TableRow key={admin.id}>
|
<TableRow key={admin.id}>
|
||||||
<TableCell className="px-4 font-medium">
|
<TableCell className="font-medium">
|
||||||
{admin.username}
|
{admin.username}
|
||||||
<span className="ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-muted/50 text-muted-foreground border border-border">
|
<span className="ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-muted/50 text-muted-foreground border border-border">
|
||||||
{t("admin.adminBadge")}
|
{t("admin.adminBadge")}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="px-4">
|
<TableCell>
|
||||||
{admin.is_oidc
|
{admin.is_oidc
|
||||||
? t("admin.external")
|
? t("admin.external")
|
||||||
: t("admin.local")}
|
: t("admin.local")}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="px-4">
|
<TableCell>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -1622,6 +1712,97 @@ export function AdminSettings({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Role Management Dialog */}
|
||||||
|
<Dialog open={rolesDialogOpen} onOpenChange={setRolesDialogOpen}>
|
||||||
|
<DialogContent className="max-w-2xl bg-dark-bg border-2 border-dark-border">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t("rbac.manageRoles")}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t("rbac.manageRolesFor", { username: selectedUser?.username || "" })}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{rolesLoading ? (
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
{t("common.loading")}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-6 py-4">
|
||||||
|
{/* Current Roles */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label>{t("rbac.currentRoles")}</Label>
|
||||||
|
{userRoles.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t("rbac.noRolesAssigned")}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{userRoles.map((userRole) => (
|
||||||
|
<div
|
||||||
|
key={userRole.roleId}
|
||||||
|
className="flex items-center justify-between p-3 border rounded-lg"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{t(userRole.roleDisplayName)}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{userRole.roleName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{userRole.roleName !== "admin" && userRole.roleName !== "user" && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => handleRemoveRole(userRole.roleId)}
|
||||||
|
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Assign New Role */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label>{t("rbac.assignNewRole")}</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{availableRoles
|
||||||
|
.filter(
|
||||||
|
(role) =>
|
||||||
|
!role.isSystem &&
|
||||||
|
!userRoles.some((ur) => ur.roleId === role.id),
|
||||||
|
)
|
||||||
|
.map((role) => (
|
||||||
|
<Button
|
||||||
|
key={role.id}
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleAssignRole(role.id)}
|
||||||
|
>
|
||||||
|
{t(role.displayName)}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setRolesDialogOpen(false)}
|
||||||
|
>
|
||||||
|
{t("common.close")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -298,21 +298,23 @@ export function RoleManagement(): React.ReactElement {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => handleEditRole(role)}
|
|
||||||
>
|
|
||||||
<Edit className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
{!role.isSystem && (
|
{!role.isSystem && (
|
||||||
<Button
|
<>
|
||||||
size="sm"
|
<Button
|
||||||
variant="ghost"
|
size="sm"
|
||||||
onClick={() => handleDeleteRole(role)}
|
variant="ghost"
|
||||||
>
|
onClick={() => handleEditRole(role)}
|
||||||
<Trash2 className="h-4 w-4" />
|
>
|
||||||
</Button>
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => handleDeleteRole(role)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -339,7 +341,7 @@ export function RoleManagement(): React.ReactElement {
|
|||||||
|
|
||||||
{/* Create/Edit Role Dialog */}
|
{/* Create/Edit Role Dialog */}
|
||||||
<Dialog open={roleDialogOpen} onOpenChange={setRoleDialogOpen}>
|
<Dialog open={roleDialogOpen} onOpenChange={setRoleDialogOpen}>
|
||||||
<DialogContent>
|
<DialogContent className="sm:max-w-[500px] bg-dark-bg border-2 border-dark-border">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{editingRole ? t("rbac.editRole") : t("rbac.createRole")}
|
{editingRole ? t("rbac.editRole") : t("rbac.createRole")}
|
||||||
@@ -351,7 +353,7 @@ export function RoleManagement(): React.ReactElement {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-6 py-4">
|
||||||
{!editingRole && (
|
{!editingRole && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="role-name">{t("rbac.roleName")}</Label>
|
<Label htmlFor="role-name">{t("rbac.roleName")}</Label>
|
||||||
@@ -403,13 +405,13 @@ export function RoleManagement(): React.ReactElement {
|
|||||||
|
|
||||||
{/* Assign Role Dialog */}
|
{/* Assign Role Dialog */}
|
||||||
<Dialog open={assignDialogOpen} onOpenChange={setAssignDialogOpen}>
|
<Dialog open={assignDialogOpen} onOpenChange={setAssignDialogOpen}>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl bg-dark-bg border-2 border-dark-border">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t("rbac.assignRoles")}</DialogTitle>
|
<DialogTitle>{t("rbac.assignRoles")}</DialogTitle>
|
||||||
<DialogDescription>{t("rbac.assignRolesDescription")}</DialogDescription>
|
<DialogDescription>{t("rbac.assignRolesDescription")}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-6 py-4">
|
||||||
{/* User Selection */}
|
{/* User Selection */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="user-select">{t("rbac.selectUser")}</Label>
|
<Label htmlFor="user-select">{t("rbac.selectUser")}</Label>
|
||||||
|
|||||||
@@ -1183,9 +1183,11 @@ export function HostManagerEditor({
|
|||||||
<TabsTrigger value="statistics">
|
<TabsTrigger value="statistics">
|
||||||
{t("hosts.statistics")}
|
{t("hosts.statistics")}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="sharing">
|
{!editingHost?.isShared && (
|
||||||
{t("rbac.sharing")}
|
<TabsTrigger value="sharing">
|
||||||
</TabsTrigger>
|
{t("rbac.sharing")}
|
||||||
|
</TabsTrigger>
|
||||||
|
)}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="general" className="pt-2">
|
<TabsContent value="general" className="pt-2">
|
||||||
<FormLabel className="mb-3 font-bold">
|
<FormLabel className="mb-3 font-bold">
|
||||||
@@ -3316,13 +3318,15 @@ export function HostManagerEditor({
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
<footer className="shrink-0 w-full pb-0">
|
<footer className="shrink-0 w-full pb-0">
|
||||||
<Separator className="p-0.25" />
|
<Separator className="p-0.25" />
|
||||||
<Button className="translate-y-2" type="submit" variant="outline">
|
{!(editingHost?.permissionLevel === "view") && (
|
||||||
{editingHost
|
<Button className="translate-y-2" type="submit" variant="outline">
|
||||||
? editingHost.id
|
{editingHost
|
||||||
? t("hosts.updateHost")
|
? editingHost.id
|
||||||
: t("hosts.cloneHost")
|
? t("hosts.updateHost")
|
||||||
: t("hosts.addHost")}
|
: t("hosts.cloneHost")
|
||||||
</Button>
|
: t("hosts.addHost")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</footer>
|
</footer>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import { useConfirmation } from "@/hooks/use-confirmation.ts";
|
|||||||
import {
|
import {
|
||||||
getRoles,
|
getRoles,
|
||||||
getUserList,
|
getUserList,
|
||||||
|
getUserInfo,
|
||||||
shareHost,
|
shareHost,
|
||||||
getHostAccess,
|
getHostAccess,
|
||||||
revokeHostAccess,
|
revokeHostAccess,
|
||||||
@@ -55,7 +56,6 @@ interface User {
|
|||||||
|
|
||||||
const PERMISSION_LEVELS = [
|
const PERMISSION_LEVELS = [
|
||||||
{ value: "view", labelKey: "rbac.view" },
|
{ value: "view", labelKey: "rbac.view" },
|
||||||
{ value: "use", labelKey: "rbac.use" },
|
|
||||||
{ value: "manage", labelKey: "rbac.manage" },
|
{ value: "manage", labelKey: "rbac.manage" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -71,13 +71,14 @@ export function HostSharingTab({
|
|||||||
const [selectedRoleId, setSelectedRoleId] = React.useState<number | null>(
|
const [selectedRoleId, setSelectedRoleId] = React.useState<number | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [permissionLevel, setPermissionLevel] = React.useState("use");
|
const [permissionLevel, setPermissionLevel] = React.useState("view");
|
||||||
const [expiresInHours, setExpiresInHours] = React.useState<string>("");
|
const [expiresInHours, setExpiresInHours] = React.useState<string>("");
|
||||||
|
|
||||||
const [roles, setRoles] = React.useState<Role[]>([]);
|
const [roles, setRoles] = React.useState<Role[]>([]);
|
||||||
const [users, setUsers] = React.useState<User[]>([]);
|
const [users, setUsers] = React.useState<User[]>([]);
|
||||||
const [accessList, setAccessList] = React.useState<AccessRecord[]>([]);
|
const [accessList, setAccessList] = React.useState<AccessRecord[]>([]);
|
||||||
const [loading, setLoading] = React.useState(false);
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
const [currentUserId, setCurrentUserId] = React.useState<string>("");
|
||||||
|
|
||||||
// Load roles
|
// Load roles
|
||||||
const loadRoles = React.useCallback(async () => {
|
const loadRoles = React.useCallback(async () => {
|
||||||
@@ -131,6 +132,19 @@ export function HostSharingTab({
|
|||||||
}
|
}
|
||||||
}, [loadRoles, loadUsers, loadAccessList, isNewHost]);
|
}, [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
|
// Share host
|
||||||
const handleShare = async () => {
|
const handleShare = async () => {
|
||||||
if (!hostId) {
|
if (!hostId) {
|
||||||
@@ -148,6 +162,12 @@ export function HostSharingTab({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent sharing with self
|
||||||
|
if (shareType === "user" && selectedUserId === currentUserId) {
|
||||||
|
toast.error(t("rbac.cannotShareWithSelf"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await shareHost(hostId, {
|
await shareHost(hostId, {
|
||||||
targetType: shareType,
|
targetType: shareType,
|
||||||
@@ -298,13 +318,18 @@ export function HostSharingTab({
|
|||||||
{/* Expiration */}
|
{/* Expiration */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="expires-in">
|
<Label htmlFor="expires-in">
|
||||||
{t("rbac.expiresIn")} ({t("rbac.hours")})
|
{t("rbac.durationHours")}
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="expires-in"
|
id="expires-in"
|
||||||
type="number"
|
type="number"
|
||||||
value={expiresInHours}
|
value={expiresInHours}
|
||||||
onChange={(e) => setExpiresInHours(e.target.value)}
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (value === "" || /^\d+$/.test(value)) {
|
||||||
|
setExpiresInHours(value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
placeholder={t("rbac.neverExpires")}
|
placeholder={t("rbac.neverExpires")}
|
||||||
min="1"
|
min="1"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user