diff --git a/src/modules/rest/api/users.rs b/src/modules/rest/api/users.rs index 84a0397..4018f8b 100644 --- a/src/modules/rest/api/users.rs +++ b/src/modules/rest/api/users.rs @@ -27,7 +27,7 @@ use crate::modules::users::payload::{ RoleCreateRequest, RoleUpdateRequest, UserCreateRequest, UserUpdateRequest, }; use crate::modules::users::permissions::Permission; -use crate::modules::users::role::UserRole; +use crate::modules::users::role::{RoleType, UserRole}; use crate::modules::users::view::UserView; use crate::modules::users::UserModel; use poem::web::Path; @@ -101,10 +101,7 @@ impl UsersApi { let roles = UserRole::list_all().await?; let role_lookup: BTreeMap = roles.into_iter().map(|r| (r.id, r)).collect(); let users = UserModel::list_all().await?; - let users = users - .into_iter() - .map(|u| u.to_view(&role_lookup)) - .collect(); + let users = users.into_iter().map(|u| u.to_view(&role_lookup)).collect(); Ok(Json(users)) } @@ -214,4 +211,21 @@ impl UsersApi { Ok(Json(minimal_list)) } + + #[oai( + path = "/list-account-roles", + method = "get", + operation_id = "list_account_roles" + )] + async fn list_account_roles(&self, context: ClientContext) -> ApiResult>> { + context + .require_permission(None, Permission::USER_VIEW) + .await?; + let all = UserRole::list_all().await?; + Ok(Json( + all.into_iter() + .filter(|r| matches!(r.role_type, RoleType::Account)) + .collect(), + )) + } } diff --git a/src/modules/users/permissions.rs b/src/modules/users/permissions.rs index b55fbb2..9a48ce7 100644 --- a/src/modules/users/permissions.rs +++ b/src/modules/users/permissions.rs @@ -53,7 +53,8 @@ impl Permission { /// Create, modify, and delete all users and their roles (Admin only). pub const USER_MANAGE: &str = "user:manage"; - /// View the minimal user list and basic profiles (Managers and Admins). + /// View the minimal user list, basic user profiles, + /// including visibility into account-level roles (Managers and Admins). pub const USER_VIEW: &str = "user:view"; /// View and revoke all access tokens in the system. diff --git a/web/src/api/users/api.ts b/web/src/api/users/api.ts index 20afd00..5069cfa 100644 --- a/web/src/api/users/api.ts +++ b/web/src/api/users/api.ts @@ -180,6 +180,11 @@ export const list_minimal_users = async () => { return response.data; }; +export const list_account_roles = async () => { + const response = await axiosInstance.get("/api/v1/list-account-roles"); + return response.data; +}; + export const remove_user = async (id: number) => { const response = await axiosInstance.delete(`/api/v1/users/${id}`); return response.data; diff --git a/web/src/features/accounts/components/access-assignment-dialog.tsx b/web/src/features/accounts/components/access-assignment-dialog.tsx index 2d9aac2..f9322fd 100644 --- a/web/src/features/accounts/components/access-assignment-dialog.tsx +++ b/web/src/features/accounts/components/access-assignment-dialog.tsx @@ -20,7 +20,7 @@ import React from 'react' import { z } from 'zod' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' -import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { Loader2, ShieldCheck, Users, Search } from 'lucide-react' import { useTranslation } from 'react-i18next' @@ -52,9 +52,8 @@ import { Button } from '@/components/ui/button' import { ScrollArea } from '@/components/ui/scroll-area' import { Input } from '@/components/ui/input' import { useToast } from '@/hooks/use-toast' -import { useRoles } from '@/hooks/use-roles' -import { useMinimalUsers } from '@/hooks/use-minimal-users' import { access_assign, AccountModel } from '@/api/account/api' +import { list_account_roles, list_minimal_users, MinimalUser, UserRole } from '@/api/users/api' interface Props { currentRow: AccountModel @@ -71,12 +70,23 @@ export function AccountAccessAssignmentDialog({ const { toast } = useToast() const queryClient = useQueryClient() - const { accountRoles, isLoading: isLoadingRoles } = useRoles() - const { users, isLoading: isLoadingUsers } = useMinimalUsers() + const { data: roles, isLoading: isLoadingRoles } = useQuery({ + queryKey: ['account-role-list'], + queryFn: list_account_roles, + staleTime: 5 * 60 * 1000, + enabled: open, + }); + + + const { data: users, isLoading: isLoadingUsers } = useQuery({ + queryKey: ['minimal-user-list'], + queryFn: list_minimal_users, + staleTime: 5 * 60 * 1000, + enabled: open, + }); const [keyword, setKeyword] = React.useState('') - // 1. 定义校验 Schema (集成国际化错误提示) const assignmentSchema = z.object({ account_ids: z.array(z.number()), user_ids: z.array(z.number()).min(1, { @@ -101,7 +111,7 @@ export function AccountAccessAssignmentDialog({ const filteredUsers = React.useMemo(() => { if (!keyword.trim()) return users const lowerKeyword = keyword.toLowerCase() - return users.filter( + return users!.filter( (user) => user.username.toLowerCase().includes(lowerKeyword) || user.email.toLowerCase().includes(lowerKeyword) @@ -170,7 +180,7 @@ export function AccountAccessAssignmentDialog({ - {accountRoles.map((role) => ( + {roles && roles.map((role) => ( {role.name} @@ -206,12 +216,12 @@ export function AccountAccessAssignmentDialog({ ) : (
- {filteredUsers.length === 0 ? ( + {filteredUsers && (filteredUsers.length === 0 ? (
{t('accounts.access_control.user_empty')}
) : ( - filteredUsers.map((user) => ( + filteredUsers!.map((user) => ( )) - )} + ))}
)} diff --git a/web/src/hooks/use-minimal-users.ts b/web/src/hooks/use-minimal-users.ts deleted file mode 100644 index 15240db..0000000 --- a/web/src/hooks/use-minimal-users.ts +++ /dev/null @@ -1,51 +0,0 @@ -// -// Copyright (c) 2025 rustmailer.com (https://rustmailer.com) -// -// This file is part of the Bichon Email Archiving Project -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - - -import { list_minimal_users, MinimalUser } from '@/api/users/api' -import { useQuery } from '@tanstack/react-query' - -export function useMinimalUsers() { - const query = useQuery({ - queryKey: ['minimal-user-list'], - queryFn: list_minimal_users, - staleTime: 5 * 60 * 1000, - }) - - const users = query.data ?? [] - const userMap = users.reduce((map, user) => { - map[user.id] = user - return map - }, {} as Record) - - - const getUsername = (id: number) => userMap[id]?.username ?? '' - const getEmail = (id: number) => userMap[id]?.email ?? '' - const getUser = (id: number) => userMap[id] ?? null - const hasUser = (id: number) => !!userMap[id] - - return { - ...query, - users, - userMap, - getUsername, - getEmail, - getUser, - hasUser, - } -} \ No newline at end of file