Fix RBAC role system bugs and improve UX (#446)

* Fix RBAC role system bugs and improve UX

- Fix user list dropdown selection in host sharing
- Fix role sharing permissions to include role-based access
- Fix translation template interpolation for success messages
- Standardize system roles to admin and user only
- Auto-assign user role to new registrations
- Remove blocking confirmation dialogs in modal contexts
- Add missing i18n keys for common actions
- Fix button type to prevent unintended form submissions

* 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

* Add empty state message for role assignment

- Display helpful message when no custom roles available
- Clarify that system roles are auto-assigned
- Add noCustomRolesToAssign translation in English and Chinese

* fix: Prevent credential sharing errors for shared hosts

- Skip credential resolution for shared hosts with credential authentication
  to prevent decryption errors (credentials are encrypted per-user)
- Add warning alert in sharing tab when host uses credential authentication
- Inform users that shared users cannot connect to credential-based hosts
- Add translations for credential sharing warning (EN/ZH)

This prevents authentication failures when sharing hosts configured
with credential authentication while maintaining security by keeping
credentials isolated per user.

* feat: Improve rbac UI and fixes some bugs

---------

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
This commit was merged in pull request #446.
This commit is contained in:
ZacharyZcR
2025-12-20 10:13:36 +08:00
committed by GitHub
parent 1f168c6f97
commit 94651107c1
20 changed files with 4389 additions and 266 deletions

View File

@@ -12,6 +12,48 @@ import type {
DockerLogOptions,
DockerValidation,
} from "../types/index.js";
// ============================================================================
// RBAC TYPE DEFINITIONS
// ============================================================================
export interface Role {
id: number;
name: string;
displayName: string;
description: string | null;
isSystem: boolean;
permissions: string | null;
createdAt: string;
updatedAt: string;
}
export interface UserRole {
userId: string;
roleId: number;
roleName: string;
roleDisplayName: string;
grantedBy: string;
grantedByUsername: string;
grantedAt: string;
}
export interface AccessRecord {
id: number;
targetType: "user" | "role";
userId: string | null;
roleId: number | null;
username: string | null;
roleName: string | null;
roleDisplayName: string | null;
grantedBy: string;
grantedByUsername: string;
permissionLevel: string;
expiresAt: string | null;
createdAt: string;
lastAccessedAt: string | null;
accessCount: number;
}
import {
apiLogger,
authLogger,
@@ -599,6 +641,9 @@ function initializeApiInstances() {
// Homepage API (port 30006)
homepageApi = createApiInstance(getApiUrl("", 30006), "HOMEPAGE");
// RBAC API (port 30001)
rbacApi = createApiInstance(getApiUrl("", 30001), "RBAC");
// Docker Management API (port 30007)
dockerApi = createApiInstance(getApiUrl("/docker", 30007), "DOCKER");
}
@@ -621,6 +666,9 @@ export let authApi: AxiosInstance;
// Homepage API (port 30006)
export let homepageApi: AxiosInstance;
// RBAC API (port 30001)
export let rbacApi: AxiosInstance;
// Docker Management API (port 30007)
export let dockerApi: AxiosInstance;
@@ -3132,6 +3180,129 @@ export async function unlinkOIDCFromPasswordAccount(
}
}
// ============================================================================
// RBAC MANAGEMENT
// ============================================================================
// Role Management
export async function getRoles(): Promise<{ roles: Role[] }> {
try {
const response = await rbacApi.get("/rbac/roles");
return response.data;
} catch (error) {
throw handleApiError(error, "fetch roles");
}
}
export async function createRole(roleData: {
name: string;
displayName: string;
description?: string | null;
}): Promise<{ role: Role }> {
try {
const response = await rbacApi.post("/rbac/roles", roleData);
return response.data;
} catch (error) {
throw handleApiError(error, "create role");
}
}
export async function updateRole(
roleId: number,
roleData: {
displayName?: string;
description?: string | null;
},
): Promise<{ role: Role }> {
try {
const response = await rbacApi.put(`/rbac/roles/${roleId}`, roleData);
return response.data;
} catch (error) {
throw handleApiError(error, "update role");
}
}
export async function deleteRole(roleId: number): Promise<{ success: boolean }> {
try {
const response = await rbacApi.delete(`/rbac/roles/${roleId}`);
return response.data;
} catch (error) {
throw handleApiError(error, "delete role");
}
}
// User-Role Management
export async function getUserRoles(userId: string): Promise<{ roles: UserRole[] }> {
try {
const response = await rbacApi.get(`/rbac/users/${userId}/roles`);
return response.data;
} catch (error) {
throw handleApiError(error, "fetch user roles");
}
}
export async function assignRoleToUser(
userId: string,
roleId: number,
): Promise<{ success: boolean }> {
try {
const response = await rbacApi.post(`/rbac/users/${userId}/roles`, { roleId });
return response.data;
} catch (error) {
throw handleApiError(error, "assign role to user");
}
}
export async function removeRoleFromUser(
userId: string,
roleId: number,
): Promise<{ success: boolean }> {
try {
const response = await rbacApi.delete(`/rbac/users/${userId}/roles/${roleId}`);
return response.data;
} catch (error) {
throw handleApiError(error, "remove role from user");
}
}
// Host Sharing Management
export async function shareHost(
hostId: number,
shareData: {
targetType: "user" | "role";
targetUserId?: string;
targetRoleId?: number;
permissionLevel: string;
durationHours?: number;
},
): Promise<{ success: boolean }> {
try {
const response = await rbacApi.post(`/rbac/host/${hostId}/share`, shareData);
return response.data;
} catch (error) {
throw handleApiError(error, "share host");
}
}
export async function getHostAccess(hostId: number): Promise<{ accessList: AccessRecord[] }> {
try {
const response = await rbacApi.get(`/rbac/host/${hostId}/access`);
return response.data;
} catch (error) {
throw handleApiError(error, "fetch host access");
}
}
export async function revokeHostAccess(
hostId: number,
accessId: number,
): Promise<{ success: boolean }> {
try {
const response = await rbacApi.delete(`/rbac/host/${hostId}/access/${accessId}`);
return response.data;
} catch (error) {
throw handleApiError(error, "revoke host access");
// ============================================================================
// DOCKER MANAGEMENT API
// ============================================================================