mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
fix: Missing permission 'user:manage' #102
This commit is contained in:
@@ -27,7 +27,7 @@ use crate::modules::users::payload::{
|
|||||||
RoleCreateRequest, RoleUpdateRequest, UserCreateRequest, UserUpdateRequest,
|
RoleCreateRequest, RoleUpdateRequest, UserCreateRequest, UserUpdateRequest,
|
||||||
};
|
};
|
||||||
use crate::modules::users::permissions::Permission;
|
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::view::UserView;
|
||||||
use crate::modules::users::UserModel;
|
use crate::modules::users::UserModel;
|
||||||
use poem::web::Path;
|
use poem::web::Path;
|
||||||
@@ -101,10 +101,7 @@ impl UsersApi {
|
|||||||
let roles = UserRole::list_all().await?;
|
let roles = UserRole::list_all().await?;
|
||||||
let role_lookup: BTreeMap<u64, UserRole> = roles.into_iter().map(|r| (r.id, r)).collect();
|
let role_lookup: BTreeMap<u64, UserRole> = roles.into_iter().map(|r| (r.id, r)).collect();
|
||||||
let users = UserModel::list_all().await?;
|
let users = UserModel::list_all().await?;
|
||||||
let users = users
|
let users = users.into_iter().map(|u| u.to_view(&role_lookup)).collect();
|
||||||
.into_iter()
|
|
||||||
.map(|u| u.to_view(&role_lookup))
|
|
||||||
.collect();
|
|
||||||
Ok(Json(users))
|
Ok(Json(users))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,4 +211,21 @@ impl UsersApi {
|
|||||||
|
|
||||||
Ok(Json(minimal_list))
|
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<Json<Vec<UserRole>>> {
|
||||||
|
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(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ impl Permission {
|
|||||||
/// Create, modify, and delete all users and their roles (Admin only).
|
/// Create, modify, and delete all users and their roles (Admin only).
|
||||||
pub const USER_MANAGE: &str = "user:manage";
|
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";
|
pub const USER_VIEW: &str = "user:view";
|
||||||
|
|
||||||
/// View and revoke all access tokens in the system.
|
/// View and revoke all access tokens in the system.
|
||||||
|
|||||||
@@ -180,6 +180,11 @@ export const list_minimal_users = async () => {
|
|||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const list_account_roles = async () => {
|
||||||
|
const response = await axiosInstance.get<UserRole[]>("/api/v1/list-account-roles");
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
export const remove_user = async (id: number) => {
|
export const remove_user = async (id: number) => {
|
||||||
const response = await axiosInstance.delete(`/api/v1/users/${id}`);
|
const response = await axiosInstance.delete(`/api/v1/users/${id}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import React from 'react'
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
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 { Loader2, ShieldCheck, Users, Search } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
@@ -52,9 +52,8 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { useToast } from '@/hooks/use-toast'
|
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 { access_assign, AccountModel } from '@/api/account/api'
|
||||||
|
import { list_account_roles, list_minimal_users, MinimalUser, UserRole } from '@/api/users/api'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
currentRow: AccountModel
|
currentRow: AccountModel
|
||||||
@@ -71,12 +70,23 @@ export function AccountAccessAssignmentDialog({
|
|||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
const { accountRoles, isLoading: isLoadingRoles } = useRoles()
|
const { data: roles, isLoading: isLoadingRoles } = useQuery<UserRole[]>({
|
||||||
const { users, isLoading: isLoadingUsers } = useMinimalUsers()
|
queryKey: ['account-role-list'],
|
||||||
|
queryFn: list_account_roles,
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
enabled: open,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const { data: users, isLoading: isLoadingUsers } = useQuery<MinimalUser[]>({
|
||||||
|
queryKey: ['minimal-user-list'],
|
||||||
|
queryFn: list_minimal_users,
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
enabled: open,
|
||||||
|
});
|
||||||
|
|
||||||
const [keyword, setKeyword] = React.useState('')
|
const [keyword, setKeyword] = React.useState('')
|
||||||
|
|
||||||
// 1. 定义校验 Schema (集成国际化错误提示)
|
|
||||||
const assignmentSchema = z.object({
|
const assignmentSchema = z.object({
|
||||||
account_ids: z.array(z.number()),
|
account_ids: z.array(z.number()),
|
||||||
user_ids: z.array(z.number()).min(1, {
|
user_ids: z.array(z.number()).min(1, {
|
||||||
@@ -101,7 +111,7 @@ export function AccountAccessAssignmentDialog({
|
|||||||
const filteredUsers = React.useMemo(() => {
|
const filteredUsers = React.useMemo(() => {
|
||||||
if (!keyword.trim()) return users
|
if (!keyword.trim()) return users
|
||||||
const lowerKeyword = keyword.toLowerCase()
|
const lowerKeyword = keyword.toLowerCase()
|
||||||
return users.filter(
|
return users!.filter(
|
||||||
(user) =>
|
(user) =>
|
||||||
user.username.toLowerCase().includes(lowerKeyword) ||
|
user.username.toLowerCase().includes(lowerKeyword) ||
|
||||||
user.email.toLowerCase().includes(lowerKeyword)
|
user.email.toLowerCase().includes(lowerKeyword)
|
||||||
@@ -170,7 +180,7 @@ export function AccountAccessAssignmentDialog({
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{accountRoles.map((role) => (
|
{roles && roles.map((role) => (
|
||||||
<SelectItem key={role.id} value={role.id.toString()}>
|
<SelectItem key={role.id} value={role.id.toString()}>
|
||||||
{role.name}
|
{role.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -206,12 +216,12 @@ export function AccountAccessAssignmentDialog({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="p-3 space-y-1">
|
<div className="p-3 space-y-1">
|
||||||
{filteredUsers.length === 0 ? (
|
{filteredUsers && (filteredUsers.length === 0 ? (
|
||||||
<div className="text-center py-8 text-sm text-muted-foreground">
|
<div className="text-center py-8 text-sm text-muted-foreground">
|
||||||
{t('accounts.access_control.user_empty')}
|
{t('accounts.access_control.user_empty')}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
filteredUsers.map((user) => (
|
filteredUsers!.map((user) => (
|
||||||
<FormField
|
<FormField
|
||||||
key={user.id}
|
key={user.id}
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -242,7 +252,7 @@ export function AccountAccessAssignmentDialog({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
|
|
||||||
import { list_minimal_users, MinimalUser } from '@/api/users/api'
|
|
||||||
import { useQuery } from '@tanstack/react-query'
|
|
||||||
|
|
||||||
export function useMinimalUsers() {
|
|
||||||
const query = useQuery<MinimalUser[]>({
|
|
||||||
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<number, MinimalUser>)
|
|
||||||
|
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user