diff --git a/docker/nginx-https.conf b/docker/nginx-https.conf
index 5e6126bf..fb72157e 100644
--- a/docker/nginx-https.conf
+++ b/docker/nginx-https.conf
@@ -93,6 +93,15 @@ http {
proxy_set_header X-Forwarded-Proto $scheme;
}
+ location ~ ^/rbac(/.*)?$ {
+ proxy_pass http://127.0.0.1:30001;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
location ~ ^/credentials(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
diff --git a/docker/nginx.conf b/docker/nginx.conf
index db5546f0..eea71293 100644
--- a/docker/nginx.conf
+++ b/docker/nginx.conf
@@ -90,6 +90,15 @@ http {
proxy_set_header X-Forwarded-Proto $scheme;
}
+ location ~ ^/rbac(/.*)?$ {
+ proxy_pass http://127.0.0.1:30001;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
location ~ ^/credentials(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
diff --git a/src/backend/database/routes/rbac.ts b/src/backend/database/routes/rbac.ts
index 80e6efed..64d21ade 100644
--- a/src/backend/database/routes/rbac.ts
+++ b/src/backend/database/routes/rbac.ts
@@ -52,15 +52,21 @@ router.post(
// Validate target type
if (!["user", "role"].includes(targetType)) {
- return res.status(400).json({ error: "Invalid target type. Must be 'user' or 'role'" });
+ return res
+ .status(400)
+ .json({ error: "Invalid target type. Must be 'user' or 'role'" });
}
// Validate required fields based on target type
if (targetType === "user" && !isNonEmptyString(targetUserId)) {
- return res.status(400).json({ error: "Target user ID is required when sharing with user" });
+ return res
+ .status(400)
+ .json({ error: "Target user ID is required when sharing with user" });
}
if (targetType === "role" && !targetRoleId) {
- return res.status(400).json({ error: "Target role ID is required when sharing with role" });
+ return res
+ .status(400)
+ .json({ error: "Target role ID is required when sharing with role" });
}
// Verify user owns the host
@@ -104,7 +110,11 @@ router.post(
// Calculate expiry time
let expiresAt: string | null = null;
- if (durationHours && typeof durationHours === "number" && durationHours > 0) {
+ if (
+ durationHours &&
+ typeof durationHours === "number" &&
+ durationHours > 0
+ ) {
const expiryDate = new Date();
expiryDate.setHours(expiryDate.getHours() + durationHours);
expiresAt = expiryDate.toISOString();
@@ -299,7 +309,7 @@ router.get(
.orderBy(desc(hostAccess.createdAt));
// Format access list with type information
- const accessList = rawAccessList.map(access => ({
+ const accessList = rawAccessList.map((access) => ({
id: access.id,
targetType: access.userId ? "user" : "role",
userId: access.userId,
@@ -361,10 +371,7 @@ router.get(
.where(
and(
eq(hostAccess.userId, userId),
- or(
- isNull(hostAccess.expiresAt),
- gte(hostAccess.expiresAt, now),
- ),
+ or(isNull(hostAccess.expiresAt), gte(hostAccess.expiresAt, now)),
),
)
.orderBy(desc(hostAccess.createdAt));
@@ -729,15 +736,20 @@ router.post(
return res.status(404).json({ error: "Role not found" });
}
+ // Prevent manual assignment of system roles
+ if (role[0].isSystem) {
+ return res.status(403).json({
+ error:
+ "System roles (admin, user) are automatically assigned and cannot be manually assigned",
+ });
+ }
+
// Check if already assigned
const existing = await db
.select()
.from(userRoles)
.where(
- and(
- eq(userRoles.userId, targetUserId),
- eq(userRoles.roleId, roleId),
- ),
+ and(eq(userRoles.userId, targetUserId), eq(userRoles.roleId, roleId)),
)
.limit(1);
@@ -794,14 +806,34 @@ router.delete(
}
try {
+ // Verify role exists and get its details
+ const role = await db
+ .select({
+ id: roles.id,
+ name: roles.name,
+ isSystem: roles.isSystem,
+ })
+ .from(roles)
+ .where(eq(roles.id, roleId))
+ .limit(1);
+
+ if (role.length === 0) {
+ return res.status(404).json({ error: "Role not found" });
+ }
+
+ // Prevent removal of system roles
+ if (role[0].isSystem) {
+ return res.status(403).json({
+ error:
+ "System roles (admin, user) are automatically assigned and cannot be removed",
+ });
+ }
+
// Delete the user-role assignment
await db
.delete(userRoles)
.where(
- and(
- eq(userRoles.userId, targetUserId),
- eq(userRoles.roleId, roleId),
- ),
+ and(eq(userRoles.userId, targetUserId), eq(userRoles.roleId, roleId)),
);
// Invalidate permission cache
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index fd18585e..8ce7fa31 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -1881,10 +1881,14 @@
"assignRoles": "Assign Roles",
"userRoleAssignment": "User-Role Assignment",
"selectUserPlaceholder": "Select a user",
+ "searchUsers": "Search users...",
+ "noUserFound": "No user found",
"currentRoles": "Current Roles",
"noRolesAssigned": "No roles assigned",
"assignNewRole": "Assign New Role",
"selectRolePlaceholder": "Select a role",
+ "searchRoles": "Search roles...",
+ "noRoleFound": "No role found",
"assign": "Assign",
"roleCreatedSuccessfully": "Role created successfully",
"roleUpdatedSuccessfully": "Role updated successfully",
@@ -1954,4 +1958,4 @@
"close": "Close",
"hostManager": "Host Manager"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json
index 234723c5..7544dd64 100644
--- a/src/locales/zh/translation.json
+++ b/src/locales/zh/translation.json
@@ -389,7 +389,7 @@
"actions": "操作",
"remove": "移除",
"revoke": "撤销",
- "create": "创建"
+ "create": "创建",
"saving": "保存中...",
"version": "Version"
},
@@ -1880,10 +1880,14 @@
"assignRoles": "分配角色",
"userRoleAssignment": "用户角色分配",
"selectUserPlaceholder": "选择用户",
+ "searchUsers": "搜索用户...",
+ "noUserFound": "未找到用户",
"currentRoles": "当前角色",
"noRolesAssigned": "未分配角色",
"assignNewRole": "分配新角色",
"selectRolePlaceholder": "选择角色",
+ "searchRoles": "搜索角色...",
+ "noRoleFound": "未找到角色",
"assign": "分配",
"roleCreatedSuccessfully": "角色创建成功",
"roleUpdatedSuccessfully": "角色更新成功",
@@ -1953,4 +1957,4 @@
"close": "关闭",
"hostManager": "主机管理器"
}
-}
\ No newline at end of file
+}
diff --git a/src/ui/desktop/admin/AdminSettings.tsx b/src/ui/desktop/admin/AdminSettings.tsx
index e15b9e73..e271db3d 100644
--- a/src/ui/desktop/admin/AdminSettings.tsx
+++ b/src/ui/desktop/admin/AdminSettings.tsx
@@ -286,7 +286,10 @@ export function AdminSettings({
};
// Role management functions
- const handleOpenRolesDialog = async (user: { id: string; username: string }) => {
+ const handleOpenRolesDialog = async (user: {
+ id: string;
+ username: string;
+ }) => {
setSelectedUser(user);
setRolesDialogOpen(true);
setRolesLoading(true);
@@ -312,7 +315,9 @@ export function AdminSettings({
try {
await assignRoleToUser(selectedUser.id, roleId);
- toast.success(t("rbac.roleAssignedSuccessfully", { username: selectedUser.username }));
+ toast.success(
+ t("rbac.roleAssignedSuccessfully", { username: selectedUser.username }),
+ );
// Reload user roles
const rolesResponse = await getUserRoles(selectedUser.id);
@@ -325,16 +330,11 @@ export function AdminSettings({
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 }));
+ toast.success(
+ t("rbac.roleRemovedSuccessfully", { username: selectedUser.username }),
+ );
// Reload user roles
const rolesResponse = await getUserRoles(selectedUser.id);
@@ -1162,102 +1162,92 @@ export function AdminSettings({
{t("admin.loadingUsers")}
) : (
-
-
-
-
-
- {t("admin.username")}
-
-
- {t("admin.type")}
-
-
- {t("admin.actions")}
-
-
-
-
- {users.map((user) => (
-
-
- {user.username}
- {user.is_admin && (
-
- {t("admin.adminBadge")}
-
- )}
-
-
- {user.is_oidc && user.password_hash
- ? "Dual Auth"
- : user.is_oidc
- ? t("admin.external")
- : t("admin.local")}
-
-
-
- {user.is_oidc && !user.password_hash && (
-
- )}
- {user.is_oidc && user.password_hash && (
-
- )}
+
+
+
+ {t("admin.username")}
+ {t("admin.type")}
+ {t("admin.actions")}
+
+
+
+ {users.map((user) => (
+
+
+ {user.username}
+ {user.is_admin && (
+
+ {t("admin.adminBadge")}
+
+ )}
+
+
+ {user.is_oidc && user.password_hash
+ ? "Dual Auth"
+ : user.is_oidc
+ ? t("admin.external")
+ : t("admin.local")}
+
+
+
+ {user.is_oidc && !user.password_hash && (
+ )}
+ {user.is_oidc && user.password_hash && (
-
-
-
- ))}
-
-
-
+ )}
+
+
+
+
+
+ ))}
+
+
)}
@@ -1284,115 +1274,107 @@ export function AdminSettings({
No active sessions found.
) : (
-
-
-
-
-
- Device
- User
- Created
- Last Active
- Expires
-
- {t("admin.actions")}
-
+
+
+
+ Device
+ User
+ Created
+ Last Active
+ Expires
+ {t("admin.actions")}
+
+
+
+ {sessions.map((session) => {
+ const DeviceIcon =
+ session.deviceType === "desktop"
+ ? Monitor
+ : session.deviceType === "mobile"
+ ? Smartphone
+ : Globe;
+
+ const createdDate = new Date(session.createdAt);
+ const lastActiveDate = new Date(session.lastActiveAt);
+ const expiresDate = new Date(session.expiresAt);
+
+ const formatDate = (date: Date) =>
+ date.toLocaleDateString() +
+ " " +
+ date.toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+
+ return (
+
+
+
+
+
+
+ {session.deviceInfo}
+
+ {session.isRevoked && (
+
+ Revoked
+
+ )}
+
+
+
+
+ {session.username || session.userId}
+
+
+ {formatDate(createdDate)}
+
+
+ {formatDate(lastActiveDate)}
+
+
+ {formatDate(expiresDate)}
+
+
+
+
+ {session.username && (
+
+ )}
+
+
-
-
- {sessions.map((session) => {
- const DeviceIcon =
- session.deviceType === "desktop"
- ? Monitor
- : session.deviceType === "mobile"
- ? Smartphone
- : Globe;
-
- const createdDate = new Date(session.createdAt);
- const lastActiveDate = new Date(
- session.lastActiveAt,
- );
- const expiresDate = new Date(session.expiresAt);
-
- const formatDate = (date: Date) =>
- date.toLocaleDateString() +
- " " +
- date.toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- });
-
- return (
-
-
-
-
-
-
- {session.deviceInfo}
-
- {session.isRevoked && (
-
- Revoked
-
- )}
-
-
-
-
- {session.username || session.userId}
-
-
- {formatDate(createdDate)}
-
-
- {formatDate(lastActiveDate)}
-
-
- {formatDate(expiresDate)}
-
-
-
-
- {session.username && (
-
- )}
-
-
-
- );
- })}
-
-
-
-
+ );
+ })}
+
+
)}
@@ -1440,55 +1422,47 @@ export function AdminSettings({
{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")}
-
-
-
-
-
- ))}
-
-
-
+
+
+
+ {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")}
+
+
+
+
+
+ ))}
+
+
@@ -1719,7 +1693,9 @@ export function AdminSettings({
{t("rbac.manageRoles")}
- {t("rbac.manageRolesFor", { username: selectedUser?.username || "" })}
+ {t("rbac.manageRolesFor", {
+ username: selectedUser?.username || "",
+ })}
@@ -1737,19 +1713,25 @@ export function AdminSettings({
{t("rbac.noRolesAssigned")}
) : (
-
+
{userRoles.map((userRole) => (
-
{t(userRole.roleDisplayName)}
+
+ {t(userRole.roleDisplayName)}
+
{userRole.roleName}
- {userRole.roleName !== "admin" && userRole.roleName !== "user" && (
+ {userRole.isSystem ? (
+
+ {t("rbac.systemRole")}
+
+ ) : (