fix(frontend): eliminate boot race between role restore and route guard
Root cause (pre-existing): user state initialized with default role 'developer' while JWT role restoration happened via render-phase setState ~1 render later; first guard pass evaluated tickets/workflow against the developer writable-datasource gate and bounced admins to dashboard on direct load. Fix: initialize user state lazily from the JWT in one pass (restoreUserFromToken) - first render already carries the correct role. Verified in browser: direct loads of /tickets and /workflow stay put; URL query state survives refresh; expired session redirects to /login?redirect=<path> and login returns to the original page.
This commit is contained in:
1 parent
d867c276d4
commit
6945c53879
1 file changed
+262
-204
+262
-204
@@ -18,12 +18,24 @@ export type { UserRole };
|
|||||||
|
|
||||||
/** 细粒度权限类型 */
|
/** 细粒度权限类型 */
|
||||||
export type PermissionAction =
|
export type PermissionAction =
|
||||||
| 'ticket.create' | 'ticket.approve' | 'ticket.execute' | 'ticket.rollback' | 'ticket.delete'
|
| 'ticket.create'
|
||||||
| 'query.create' | 'query.export'
|
| 'ticket.approve'
|
||||||
| 'instance.add' | 'instance.edit' | 'instance.delete'
|
| 'ticket.execute'
|
||||||
| 'user.manage' | 'role.manage' | 'rule.manage'
|
| 'ticket.rollback'
|
||||||
| 'workflow.manage' | 'config.manage'
|
| 'ticket.delete'
|
||||||
| 'announcement.manage' | 'auto-task.manage' | 'instance-group.manage';
|
| 'query.create'
|
||||||
|
| 'query.export'
|
||||||
|
| 'instance.add'
|
||||||
|
| 'instance.edit'
|
||||||
|
| 'instance.delete'
|
||||||
|
| 'user.manage'
|
||||||
|
| 'role.manage'
|
||||||
|
| 'rule.manage'
|
||||||
|
| 'workflow.manage'
|
||||||
|
| 'config.manage'
|
||||||
|
| 'announcement.manage'
|
||||||
|
| 'auto-task.manage'
|
||||||
|
| 'instance-group.manage';
|
||||||
|
|
||||||
/** 数据源级权限 */
|
/** 数据源级权限 */
|
||||||
export interface DataSourcePermission {
|
export interface DataSourcePermission {
|
||||||
@@ -75,14 +87,14 @@ interface AuthContextType {
|
|||||||
instanceId: number,
|
instanceId: number,
|
||||||
action: 'read' | 'write' | 'delete' | 'export',
|
action: 'read' | 'write' | 'delete' | 'export',
|
||||||
database?: string,
|
database?: string,
|
||||||
table?: string
|
table?: string,
|
||||||
) => boolean;
|
) => boolean;
|
||||||
/** 批量检查数据源权限 */
|
/** 批量检查数据源权限 */
|
||||||
checkDataSourcePermissions: (
|
checkDataSourcePermissions: (
|
||||||
instanceId: number,
|
instanceId: number,
|
||||||
actions: ('read' | 'write' | 'delete' | 'export')[],
|
actions: ('read' | 'write' | 'delete' | 'export')[],
|
||||||
database?: string,
|
database?: string,
|
||||||
table?: string
|
table?: string,
|
||||||
) => Record<string, boolean>;
|
) => Record<string, boolean>;
|
||||||
/** 刷新用户权限 */
|
/** 刷新用户权限 */
|
||||||
refreshPermissions: () => Promise<void>;
|
refreshPermissions: () => Promise<void>;
|
||||||
@@ -140,38 +152,42 @@ const CACHE_TTL = 5 * 60 * 1000; // 5分钟
|
|||||||
|
|
||||||
const AuthContext = createContext<AuthContextType | null>(null);
|
const AuthContext = createContext<AuthContextType | null>(null);
|
||||||
|
|
||||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
/** 从 JWT 惰性恢复基本用户信息(页面刷新时的首帧即有正确角色,杜绝守卫竞态) */
|
||||||
const [user, setUser] = useState<AuthUser>({
|
function restoreUserFromToken(base: AuthUser): AuthUser {
|
||||||
id: defaultUser.id,
|
const token = readStorage('token');
|
||||||
username: defaultUser.username,
|
if (!token) return base;
|
||||||
email: defaultUser.email,
|
try {
|
||||||
role: defaultUser.role,
|
const payload = JSON.parse(atob(token.split('.')[1]));
|
||||||
roleName: defaultUser.roleName,
|
const role = payload.role as UserRole;
|
||||||
avatar: defaultUser.avatar,
|
if (!role) return base;
|
||||||
groupIds: [],
|
return {
|
||||||
dataSourcePermissions: [],
|
...base,
|
||||||
customPermissions: {},
|
id: Number(payload.sub) || 0,
|
||||||
});
|
role,
|
||||||
|
roleName: roleNameMap[role] || role,
|
||||||
|
avatar: role.charAt(0).toUpperCase(),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 从 token 恢复基本用户信息(页面刷新时)
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(() => {
|
const [user, setUser] = useState<AuthUser>(() =>
|
||||||
const token = readStorage('token');
|
restoreUserFromToken({
|
||||||
if (!token) return false;
|
id: defaultUser.id,
|
||||||
try {
|
username: defaultUser.username,
|
||||||
const payload = JSON.parse(atob(token.split('.')[1]));
|
email: defaultUser.email,
|
||||||
const role = payload.role as UserRole;
|
role: defaultUser.role,
|
||||||
if (role) {
|
roleName: defaultUser.roleName,
|
||||||
setUser(u => ({
|
avatar: defaultUser.avatar,
|
||||||
...u,
|
groupIds: [],
|
||||||
id: Number(payload.sub) || 0,
|
dataSourcePermissions: [],
|
||||||
role,
|
customPermissions: {},
|
||||||
roleName: roleNameMap[role] || role,
|
}),
|
||||||
avatar: role.charAt(0).toUpperCase(),
|
);
|
||||||
}));
|
|
||||||
}
|
const [isLoggedIn, setIsLoggedIn] = useState(() => readStorage('token') !== null);
|
||||||
} catch { /* ignore */ }
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
const [hasWritableDataSourceAccess, setHasWritableDataSourceAccess] = useState(false);
|
const [hasWritableDataSourceAccess, setHasWritableDataSourceAccess] = useState(false);
|
||||||
|
|
||||||
/** 权限缓存引用 */
|
/** 权限缓存引用 */
|
||||||
@@ -199,14 +215,16 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const hasWritableAccessInUserPayload = useCallback((u: AuthUser): boolean => {
|
const hasWritableAccessInUserPayload = useCallback((u: AuthUser): boolean => {
|
||||||
return Boolean(u.dataSourcePermissions?.some(ds =>
|
return Boolean(
|
||||||
ds.actions.some(action => ['write', 'delete', 'readwrite'].includes(String(action)))
|
u.dataSourcePermissions?.some((ds) =>
|
||||||
));
|
ds.actions.some((action) => ['write', 'delete', 'readwrite'].includes(String(action))),
|
||||||
|
),
|
||||||
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/** 触发权限变更事件 */
|
/** 触发权限变更事件 */
|
||||||
const emitPermissionChange = useCallback((updatedUser: AuthUser) => {
|
const emitPermissionChange = useCallback((updatedUser: AuthUser) => {
|
||||||
listenersRef.current.forEach(listener => listener(updatedUser));
|
listenersRef.current.forEach((listener) => listener(updatedUser));
|
||||||
// 触发自定义事件,供其他组件监听
|
// 触发自定义事件,供其他组件监听
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.dispatchEvent(new CustomEvent(PERMISSION_CHANGE_EVENT, { detail: updatedUser }));
|
window.dispatchEvent(new CustomEvent(PERMISSION_CHANGE_EVENT, { detail: updatedUser }));
|
||||||
@@ -247,7 +265,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
instanceApi.accessible('write')
|
instanceApi
|
||||||
|
.accessible('write')
|
||||||
.then((instances) => {
|
.then((instances) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setHasWritableDataSourceAccess((instances || []).length > 0);
|
setHasWritableDataSourceAccess((instances || []).length > 0);
|
||||||
@@ -264,47 +283,48 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
};
|
};
|
||||||
}, [isLoggedIn, user.id, user.role, user.permissionsUpdatedAt, hasWritableAccessInUserPayload, clearPermissionCache]);
|
}, [isLoggedIn, user.id, user.role, user.permissionsUpdatedAt, hasWritableAccessInUserPayload, clearPermissionCache]);
|
||||||
|
|
||||||
const login = useCallback(async (username: string, password: string): Promise<boolean> => {
|
const login = useCallback(
|
||||||
try {
|
async (username: string, password: string): Promise<boolean> => {
|
||||||
const res = await authApi.login(username, password);
|
try {
|
||||||
writeStorage('token', res.access_token);
|
const res = await authApi.login(username, password);
|
||||||
if (res.refresh_token) {
|
writeStorage('token', res.access_token);
|
||||||
writeStorage('refreshToken', res.refresh_token);
|
if (res.refresh_token) {
|
||||||
|
writeStorage('refreshToken', res.refresh_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
const u = res.user;
|
||||||
|
const role = u.role as UserRole;
|
||||||
|
|
||||||
|
// 解析后端返回的细粒度权限
|
||||||
|
const dataSourcePermissions: DataSourcePermission[] = u.data_source_permissions
|
||||||
|
? JSON.parse(u.data_source_permissions)
|
||||||
|
: [];
|
||||||
|
const customPermissions: Record<string, boolean> = u.custom_permissions ? JSON.parse(u.custom_permissions) : {};
|
||||||
|
|
||||||
|
const newUser: AuthUser = {
|
||||||
|
id: u.id,
|
||||||
|
username: u.username,
|
||||||
|
email: u.email || '',
|
||||||
|
role,
|
||||||
|
roleName: roleNameMap[role] || role,
|
||||||
|
avatar: u.display_name?.charAt(0) || username.charAt(0).toUpperCase(),
|
||||||
|
groupIds: u.group_ids || [],
|
||||||
|
dataSourcePermissions,
|
||||||
|
customPermissions,
|
||||||
|
permissionsUpdatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setUser(newUser);
|
||||||
|
setIsLoggedIn(true);
|
||||||
|
clearPermissionCache();
|
||||||
|
emitPermissionChange(newUser);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
const u = res.user;
|
[clearPermissionCache, emitPermissionChange],
|
||||||
const role = u.role as UserRole;
|
);
|
||||||
|
|
||||||
// 解析后端返回的细粒度权限
|
|
||||||
const dataSourcePermissions: DataSourcePermission[] = u.data_source_permissions
|
|
||||||
? JSON.parse(u.data_source_permissions)
|
|
||||||
: [];
|
|
||||||
const customPermissions: Record<string, boolean> = u.custom_permissions
|
|
||||||
? JSON.parse(u.custom_permissions)
|
|
||||||
: {};
|
|
||||||
|
|
||||||
const newUser: AuthUser = {
|
|
||||||
id: u.id,
|
|
||||||
username: u.username,
|
|
||||||
email: u.email || '',
|
|
||||||
role,
|
|
||||||
roleName: roleNameMap[role] || role,
|
|
||||||
avatar: u.display_name?.charAt(0) || username.charAt(0).toUpperCase(),
|
|
||||||
groupIds: u.group_ids || [],
|
|
||||||
dataSourcePermissions,
|
|
||||||
customPermissions,
|
|
||||||
permissionsUpdatedAt: Date.now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
setUser(newUser);
|
|
||||||
setIsLoggedIn(true);
|
|
||||||
clearPermissionCache();
|
|
||||||
emitPermissionChange(newUser);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}, [clearPermissionCache, emitPermissionChange]);
|
|
||||||
|
|
||||||
const logout = useCallback(() => {
|
const logout = useCallback(() => {
|
||||||
// 先取 token 再清理:服务端撤销 refresh token 需要它
|
// 先取 token 再清理:服务端撤销 refresh token 需要它
|
||||||
@@ -312,7 +332,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
removeStorage('token');
|
removeStorage('token');
|
||||||
removeStorage('refreshToken');
|
removeStorage('refreshToken');
|
||||||
if (token) {
|
if (token) {
|
||||||
authApi.logout(token).catch(() => { /* 服务端撤销失败不阻塞本地登出 */ });
|
authApi.logout(token).catch(() => {
|
||||||
|
/* 服务端撤销失败不阻塞本地登出 */
|
||||||
|
});
|
||||||
}
|
}
|
||||||
setIsLoggedIn(false);
|
setIsLoggedIn(false);
|
||||||
clearPermissionCache();
|
clearPermissionCache();
|
||||||
@@ -330,116 +352,136 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
emitPermissionChange({} as AuthUser);
|
emitPermissionChange({} as AuthUser);
|
||||||
}, [clearPermissionCache, emitPermissionChange]);
|
}, [clearPermissionCache, emitPermissionChange]);
|
||||||
|
|
||||||
const switchRole = useCallback((role: UserRole) => {
|
const switchRole = useCallback(
|
||||||
setUser(prev => {
|
(role: UserRole) => {
|
||||||
const newUser: AuthUser = {
|
setUser((prev) => {
|
||||||
...prev,
|
const newUser: AuthUser = {
|
||||||
role,
|
...prev,
|
||||||
roleName: roleNameMap[role],
|
role,
|
||||||
username: role === 'admin' ? 'admin' : role === 'dba' ? 'zhangsan' : role === 'developer' ? 'wangwu' : 'visitor',
|
roleName: roleNameMap[role],
|
||||||
email: `${role}@company.com`,
|
username:
|
||||||
avatar: role.charAt(0).toUpperCase(),
|
role === 'admin' ? 'admin' : role === 'dba' ? 'zhangsan' : role === 'developer' ? 'wangwu' : 'visitor',
|
||||||
};
|
email: `${role}@company.com`,
|
||||||
clearPermissionCache();
|
avatar: role.charAt(0).toUpperCase(),
|
||||||
emitPermissionChange(newUser);
|
};
|
||||||
return newUser;
|
clearPermissionCache();
|
||||||
});
|
emitPermissionChange(newUser);
|
||||||
}, [clearPermissionCache, emitPermissionChange]);
|
return newUser;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[clearPermissionCache, emitPermissionChange],
|
||||||
|
);
|
||||||
|
|
||||||
/** 菜单权限检查(带缓存) */
|
/** 菜单权限检查(带缓存) */
|
||||||
const hasPermission = useCallback((menuId: string): boolean => {
|
const hasPermission = useCallback(
|
||||||
if (!isLoggedIn) return false;
|
(menuId: string): boolean => {
|
||||||
|
if (!isLoggedIn) return false;
|
||||||
|
|
||||||
const cache = permissionCacheRef.current;
|
const cache = permissionCacheRef.current;
|
||||||
if (isCacheValid() && cache.menu.has(menuId)) {
|
if (isCacheValid() && cache.menu.has(menuId)) {
|
||||||
return cache.menu.get(menuId)!;
|
return cache.menu.get(menuId)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = canAccessMenu(menuId, {
|
const result = canAccessMenu(menuId, {
|
||||||
role: user.role,
|
role: user.role,
|
||||||
customPermissions: user.customPermissions,
|
customPermissions: user.customPermissions,
|
||||||
hasWritableDataSourceAccess,
|
hasWritableDataSourceAccess,
|
||||||
});
|
});
|
||||||
cache.menu.set(menuId, result);
|
cache.menu.set(menuId, result);
|
||||||
cache.timestamp = Date.now();
|
cache.timestamp = Date.now();
|
||||||
return result;
|
return result;
|
||||||
}, [user.role, user.customPermissions, hasWritableDataSourceAccess, isLoggedIn, isCacheValid]);
|
},
|
||||||
|
[user.role, user.customPermissions, hasWritableDataSourceAccess, isLoggedIn, isCacheValid],
|
||||||
|
);
|
||||||
|
|
||||||
/** 操作权限检查(带缓存,支持自定义权限覆盖) */
|
/** 操作权限检查(带缓存,支持自定义权限覆盖) */
|
||||||
const hasActionPermission = useCallback((action: PermissionAction | string): boolean => {
|
const hasActionPermission = useCallback(
|
||||||
if (!isLoggedIn) return false;
|
(action: PermissionAction | string): boolean => {
|
||||||
|
if (!isLoggedIn) return false;
|
||||||
|
|
||||||
const cache = permissionCacheRef.current;
|
const cache = permissionCacheRef.current;
|
||||||
if (isCacheValid() && cache.action.has(action)) {
|
if (isCacheValid() && cache.action.has(action)) {
|
||||||
return cache.action.get(action)!;
|
return cache.action.get(action)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = canPerformAction(action, {
|
const result = canPerformAction(
|
||||||
role: user.role,
|
action,
|
||||||
customPermissions: user.customPermissions,
|
{
|
||||||
hasWritableDataSourceAccess,
|
role: user.role,
|
||||||
}, actionPermissionMap);
|
customPermissions: user.customPermissions,
|
||||||
cache.action.set(action, result);
|
hasWritableDataSourceAccess,
|
||||||
cache.timestamp = Date.now();
|
},
|
||||||
return result;
|
actionPermissionMap,
|
||||||
}, [user.role, user.customPermissions, hasWritableDataSourceAccess, isLoggedIn, isCacheValid]);
|
);
|
||||||
|
cache.action.set(action, result);
|
||||||
|
cache.timestamp = Date.now();
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
[user.role, user.customPermissions, hasWritableDataSourceAccess, isLoggedIn, isCacheValid],
|
||||||
|
);
|
||||||
|
|
||||||
/** 数据源级权限检查 */
|
/** 数据源级权限检查 */
|
||||||
const hasDataSourcePermission = useCallback((
|
const hasDataSourcePermission = useCallback(
|
||||||
instanceId: number,
|
(
|
||||||
action: 'read' | 'write' | 'delete' | 'export',
|
instanceId: number,
|
||||||
database?: string,
|
action: 'read' | 'write' | 'delete' | 'export',
|
||||||
table?: string
|
database?: string,
|
||||||
): boolean => {
|
table?: string,
|
||||||
if (!isLoggedIn) return false;
|
): boolean => {
|
||||||
if (user.role === 'admin') return true; // 管理员拥有所有数据源权限
|
if (!isLoggedIn) return false;
|
||||||
|
if (user.role === 'admin') return true; // 管理员拥有所有数据源权限
|
||||||
|
|
||||||
const cache = permissionCacheRef.current;
|
const cache = permissionCacheRef.current;
|
||||||
const cacheKey = `${instanceId}:${action}:${database || '*'}:${table || '*'}`;
|
const cacheKey = `${instanceId}:${action}:${database || '*'}:${table || '*'}`;
|
||||||
if (isCacheValid() && cache.dataSource.has(cacheKey)) {
|
if (isCacheValid() && cache.dataSource.has(cacheKey)) {
|
||||||
return cache.dataSource.get(cacheKey)!;
|
return cache.dataSource.get(cacheKey)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
const perms = user.dataSourcePermissions || [];
|
const perms = user.dataSourcePermissions || [];
|
||||||
if (perms.length === 0) {
|
if (perms.length === 0) {
|
||||||
cache.dataSource.set(cacheKey, false);
|
cache.dataSource.set(cacheKey, false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasPerm = perms.some((ds: DataSourcePermission) => {
|
const hasPerm = perms.some((ds: DataSourcePermission) => {
|
||||||
// 检查实例ID
|
// 检查实例ID
|
||||||
if (ds.instanceId !== instanceId) return false;
|
if (ds.instanceId !== instanceId) return false;
|
||||||
|
|
||||||
// 检查操作权限
|
// 检查操作权限
|
||||||
if (!ds.actions.includes(action)) return false;
|
if (!ds.actions.includes(action)) return false;
|
||||||
|
|
||||||
// 检查数据库级别
|
// 检查数据库级别
|
||||||
if (database && ds.database && ds.database !== database) return false;
|
if (database && ds.database && ds.database !== database) return false;
|
||||||
|
|
||||||
// 检查表级别
|
// 检查表级别
|
||||||
if (table && ds.tables && ds.tables.length > 0 && !ds.tables.includes(table)) return false;
|
if (table && ds.tables && ds.tables.length > 0 && !ds.tables.includes(table)) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
cache.dataSource.set(cacheKey, hasPerm);
|
cache.dataSource.set(cacheKey, hasPerm);
|
||||||
cache.timestamp = Date.now();
|
cache.timestamp = Date.now();
|
||||||
return hasPerm;
|
return hasPerm;
|
||||||
}, [user.dataSourcePermissions, user.role, isLoggedIn, isCacheValid]);
|
},
|
||||||
|
[user.dataSourcePermissions, user.role, isLoggedIn, isCacheValid],
|
||||||
|
);
|
||||||
|
|
||||||
/** 批量检查数据源权限 */
|
/** 批量检查数据源权限 */
|
||||||
const checkDataSourcePermissions = useCallback((
|
const checkDataSourcePermissions = useCallback(
|
||||||
instanceId: number,
|
(
|
||||||
actions: ('read' | 'write' | 'delete' | 'export')[],
|
instanceId: number,
|
||||||
database?: string,
|
actions: ('read' | 'write' | 'delete' | 'export')[],
|
||||||
table?: string
|
database?: string,
|
||||||
): Record<string, boolean> => {
|
table?: string,
|
||||||
const result: Record<string, boolean> = {};
|
): Record<string, boolean> => {
|
||||||
for (const action of actions) {
|
const result: Record<string, boolean> = {};
|
||||||
result[action] = hasDataSourcePermission(instanceId, action, database, table);
|
for (const action of actions) {
|
||||||
}
|
result[action] = hasDataSourcePermission(instanceId, action, database, table);
|
||||||
return result;
|
}
|
||||||
}, [hasDataSourcePermission]);
|
return result;
|
||||||
|
},
|
||||||
|
[hasDataSourcePermission],
|
||||||
|
);
|
||||||
|
|
||||||
/** 从服务器刷新用户权限 */
|
/** 从服务器刷新用户权限 */
|
||||||
const refreshPermissions = useCallback(async (): Promise<void> => {
|
const refreshPermissions = useCallback(async (): Promise<void> => {
|
||||||
@@ -449,9 +491,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const dataSourcePermissions: DataSourcePermission[] = u.data_source_permissions
|
const dataSourcePermissions: DataSourcePermission[] = u.data_source_permissions
|
||||||
? JSON.parse(u.data_source_permissions)
|
? JSON.parse(u.data_source_permissions)
|
||||||
: [];
|
: [];
|
||||||
const customPermissions: Record<string, boolean> = u.custom_permissions
|
const customPermissions: Record<string, boolean> = u.custom_permissions ? JSON.parse(u.custom_permissions) : {};
|
||||||
? JSON.parse(u.custom_permissions)
|
|
||||||
: {};
|
|
||||||
|
|
||||||
const newUser: AuthUser = {
|
const newUser: AuthUser = {
|
||||||
...user,
|
...user,
|
||||||
@@ -475,35 +515,53 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
}, [user, clearPermissionCache, emitPermissionChange]);
|
}, [user, clearPermissionCache, emitPermissionChange]);
|
||||||
|
|
||||||
const visibleMenuItems = useMemo(() => {
|
const visibleMenuItems = useMemo(() => {
|
||||||
return menuItems.filter(item => item.roles.includes(user.role) && hasPermission(item.id));
|
return menuItems.filter((item) => item.roles.includes(user.role) && hasPermission(item.id));
|
||||||
}, [user.role, hasPermission]);
|
}, [user.role, hasPermission]);
|
||||||
|
|
||||||
/** 检查是否拥有指定角色或以上级别权限 */
|
/** 检查是否拥有指定角色或以上级别权限 */
|
||||||
const hasRoleLevel = useCallback((minRole: UserRole): boolean => {
|
const hasRoleLevel = useCallback(
|
||||||
return roleWeightMap[user.role] >= roleWeightMap[minRole];
|
(minRole: UserRole): boolean => {
|
||||||
}, [user.role]);
|
return roleWeightMap[user.role] >= roleWeightMap[minRole];
|
||||||
|
},
|
||||||
|
[user.role],
|
||||||
|
);
|
||||||
|
|
||||||
const value = useMemo(() => ({
|
const value = useMemo(
|
||||||
user,
|
() => ({
|
||||||
isLoggedIn,
|
user,
|
||||||
login,
|
isLoggedIn,
|
||||||
logout,
|
login,
|
||||||
switchRole,
|
logout,
|
||||||
hasPermission,
|
switchRole,
|
||||||
hasActionPermission,
|
hasPermission,
|
||||||
hasDataSourcePermission,
|
hasActionPermission,
|
||||||
checkDataSourcePermissions,
|
hasDataSourcePermission,
|
||||||
refreshPermissions,
|
checkDataSourcePermissions,
|
||||||
clearPermissionCache,
|
refreshPermissions,
|
||||||
visibleMenuItems,
|
clearPermissionCache,
|
||||||
isAdmin: user.role === 'admin',
|
visibleMenuItems,
|
||||||
isDBA: user.role === 'dba',
|
isAdmin: user.role === 'admin',
|
||||||
isDeveloper: user.role === 'developer',
|
isDBA: user.role === 'dba',
|
||||||
isGuest: user.role === 'auditor',
|
isDeveloper: user.role === 'developer',
|
||||||
hasRoleLevel,
|
isGuest: user.role === 'auditor',
|
||||||
}), [user, isLoggedIn, login, logout, switchRole, hasPermission, hasActionPermission,
|
hasRoleLevel,
|
||||||
hasDataSourcePermission, checkDataSourcePermissions, refreshPermissions,
|
}),
|
||||||
clearPermissionCache, visibleMenuItems, hasRoleLevel]);
|
[
|
||||||
|
user,
|
||||||
|
isLoggedIn,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
switchRole,
|
||||||
|
hasPermission,
|
||||||
|
hasActionPermission,
|
||||||
|
hasDataSourcePermission,
|
||||||
|
checkDataSourcePermissions,
|
||||||
|
refreshPermissions,
|
||||||
|
clearPermissionCache,
|
||||||
|
visibleMenuItems,
|
||||||
|
hasRoleLevel,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||||
}
|
}
|
||||||
@@ -525,7 +583,7 @@ export function useDataSourcePermission(
|
|||||||
instanceId: number,
|
instanceId: number,
|
||||||
action: 'read' | 'write' | 'delete' | 'export',
|
action: 'read' | 'write' | 'delete' | 'export',
|
||||||
database?: string,
|
database?: string,
|
||||||
table?: string
|
table?: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
const { hasDataSourcePermission } = useAuth();
|
const { hasDataSourcePermission } = useAuth();
|
||||||
return hasDataSourcePermission(instanceId, action, database, table);
|
return hasDataSourcePermission(instanceId, action, database, table);
|
||||||
|
|||||||
Reference in new issue
Block a user