mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
fix: Inconsistent permissions for /oauth2: Access restricted to Global Manager only #196
This commit is contained in:
@@ -20,8 +20,8 @@ use crate::{
|
|||||||
encrypt, id,
|
encrypt, id,
|
||||||
modules::{
|
modules::{
|
||||||
database::{
|
database::{
|
||||||
delete_impl, insert_impl, manager::DB_MANAGER, paginate_query_primary_scan_all_impl,
|
async_secondary_find_impl, delete_impl, insert_impl, manager::DB_MANAGER,
|
||||||
async_secondary_find_impl, update_impl,
|
paginate_query_primary_scan_all_impl, update_impl,
|
||||||
},
|
},
|
||||||
error::{code::ErrorCode, BichonResult},
|
error::{code::ErrorCode, BichonResult},
|
||||||
rest::response::DataPage,
|
rest::response::DataPage,
|
||||||
@@ -109,9 +109,9 @@ impl OAuth2 {
|
|||||||
|
|
||||||
self.client_id = mask.to_string();
|
self.client_id = mask.to_string();
|
||||||
self.client_secret = mask.to_string();
|
self.client_secret = mask.to_string();
|
||||||
self.auth_url = mask.to_string();
|
self.auth_url = self.auth_url.clone();
|
||||||
self.token_url = mask.to_string();
|
self.token_url = self.token_url.clone();
|
||||||
self.redirect_uri = mask.to_string();
|
self.redirect_uri = self.redirect_uri.clone();
|
||||||
|
|
||||||
self.scopes = None;
|
self.scopes = None;
|
||||||
self.extra_params = None;
|
self.extra_params = None;
|
||||||
|
|||||||
@@ -35,10 +35,10 @@ pub struct OAuth2Api;
|
|||||||
|
|
||||||
#[OpenApi(prefix_path = "/api/v1", tag = "ApiTags::OAuth2")]
|
#[OpenApi(prefix_path = "/api/v1", tag = "ApiTags::OAuth2")]
|
||||||
impl OAuth2Api {
|
impl OAuth2Api {
|
||||||
/// Retrieves the OAuth2 configuration for a specified name.
|
/// Retrieves the OAuth2 configuration for a specified id.
|
||||||
///
|
///
|
||||||
/// Requires root privileges.
|
/// Requires root privileges.
|
||||||
/// This endpoint fetches the OAuth2 configuration identified by the given name.
|
/// This endpoint fetches the OAuth2 configuration identified by the given id.
|
||||||
#[oai(
|
#[oai(
|
||||||
path = "/oauth2/:id",
|
path = "/oauth2/:id",
|
||||||
method = "get",
|
method = "get",
|
||||||
@@ -46,7 +46,7 @@ impl OAuth2Api {
|
|||||||
)]
|
)]
|
||||||
async fn get_oauth2_config(
|
async fn get_oauth2_config(
|
||||||
&self,
|
&self,
|
||||||
/// The name of the OAuth2 configuration to retrieve
|
/// The id of the OAuth2 configuration to retrieve
|
||||||
id: Path<u64>,
|
id: Path<u64>,
|
||||||
context: ClientContext,
|
context: ClientContext,
|
||||||
) -> ApiResult<Json<OAuth2>> {
|
) -> ApiResult<Json<OAuth2>> {
|
||||||
@@ -57,17 +57,9 @@ impl OAuth2Api {
|
|||||||
ErrorCode::ResourceNotFound
|
ErrorCode::ResourceNotFound
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
if context
|
if context.has_permission(None, Permission::ROOT).await {
|
||||||
.has_permission(None, Permission::ROOT)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
return Ok(Json(oauth2));
|
return Ok(Json(oauth2));
|
||||||
}
|
}
|
||||||
|
|
||||||
context
|
|
||||||
.require_permission(None, Permission::ACCOUNT_CREATE)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
oauth2.scrub_sensitive_fields();
|
oauth2.scrub_sensitive_fields();
|
||||||
Ok(Json(oauth2))
|
Ok(Json(oauth2))
|
||||||
}
|
}
|
||||||
@@ -87,9 +79,7 @@ impl OAuth2Api {
|
|||||||
id: Path<u64>,
|
id: Path<u64>,
|
||||||
context: ClientContext,
|
context: ClientContext,
|
||||||
) -> ApiResult<()> {
|
) -> ApiResult<()> {
|
||||||
context
|
context.require_permission(None, Permission::ROOT).await?;
|
||||||
.require_permission(None, Permission::ROOT)
|
|
||||||
.await?;
|
|
||||||
Ok(OAuth2::delete(id.0).await?)
|
Ok(OAuth2::delete(id.0).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,9 +98,7 @@ impl OAuth2Api {
|
|||||||
request: Json<OAuth2CreateRequest>,
|
request: Json<OAuth2CreateRequest>,
|
||||||
context: ClientContext,
|
context: ClientContext,
|
||||||
) -> ApiResult<()> {
|
) -> ApiResult<()> {
|
||||||
context
|
context.require_permission(None, Permission::ROOT).await?;
|
||||||
.require_permission(None, Permission::ROOT)
|
|
||||||
.await?;
|
|
||||||
let entity = OAuth2::new(request.0)?;
|
let entity = OAuth2::new(request.0)?;
|
||||||
Ok(entity.save().await?)
|
Ok(entity.save().await?)
|
||||||
}
|
}
|
||||||
@@ -132,9 +120,7 @@ impl OAuth2Api {
|
|||||||
payload: Json<OAuth2UpdateRequest>,
|
payload: Json<OAuth2UpdateRequest>,
|
||||||
context: ClientContext,
|
context: ClientContext,
|
||||||
) -> ApiResult<()> {
|
) -> ApiResult<()> {
|
||||||
context
|
context.require_permission(None, Permission::ROOT).await?;
|
||||||
.require_permission(None, Permission::ROOT)
|
|
||||||
.await?;
|
|
||||||
Ok(OAuth2::update(id.0, payload.0).await?)
|
Ok(OAuth2::update(id.0, payload.0).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,17 +144,10 @@ impl OAuth2Api {
|
|||||||
context: ClientContext,
|
context: ClientContext,
|
||||||
) -> ApiResult<Json<DataPage<OAuth2>>> {
|
) -> ApiResult<Json<DataPage<OAuth2>>> {
|
||||||
let mut list = OAuth2::paginate_list(page.0, page_size.0, desc.0).await?;
|
let mut list = OAuth2::paginate_list(page.0, page_size.0, desc.0).await?;
|
||||||
if context
|
if context.has_permission(None, Permission::ROOT).await {
|
||||||
.has_permission(None, Permission::ROOT)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
return Ok(Json(list));
|
return Ok(Json(list));
|
||||||
}
|
}
|
||||||
|
//Non-root users can only view masked data.
|
||||||
context
|
|
||||||
.require_permission(None, Permission::ACCOUNT_CREATE)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
for item in &mut list.items {
|
for item in &mut list.items {
|
||||||
item.scrub_sensitive_fields();
|
item.scrub_sensitive_fields();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,13 +69,8 @@ impl SystemApi {
|
|||||||
|
|
||||||
/// Get the full list of SOCKS5 proxy configurations.
|
/// Get the full list of SOCKS5 proxy configurations.
|
||||||
#[oai(method = "get", path = "/list-proxy", operation_id = "list_proxy")]
|
#[oai(method = "get", path = "/list-proxy", operation_id = "list_proxy")]
|
||||||
async fn list_proxy(&self, context: ClientContext) -> ApiResult<Json<Vec<Proxy>>> {
|
async fn list_proxy(&self, _context: ClientContext) -> ApiResult<Json<Vec<Proxy>>> {
|
||||||
context
|
//The proxy list is visible to all users.
|
||||||
.require_any_permission(vec![
|
|
||||||
(None, Permission::ACCOUNT_CREATE),
|
|
||||||
(None, Permission::ROOT),
|
|
||||||
])
|
|
||||||
.await?;
|
|
||||||
let proxies = Proxy::list_all()
|
let proxies = Proxy::list_all()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
|
.map_err(|e| raise_error!(format!("{:#?}", e), ErrorCode::InternalError))?;
|
||||||
@@ -90,9 +85,7 @@ impl SystemApi {
|
|||||||
id: Path<u64>,
|
id: Path<u64>,
|
||||||
context: ClientContext,
|
context: ClientContext,
|
||||||
) -> ApiResult<()> {
|
) -> ApiResult<()> {
|
||||||
context
|
context.require_permission(None, Permission::ROOT).await?;
|
||||||
.require_permission(None, Permission::ROOT)
|
|
||||||
.await?;
|
|
||||||
Ok(Proxy::delete(id.0).await?)
|
Ok(Proxy::delete(id.0).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,18 +97,14 @@ impl SystemApi {
|
|||||||
id: Path<u64>,
|
id: Path<u64>,
|
||||||
context: ClientContext,
|
context: ClientContext,
|
||||||
) -> ApiResult<Json<Proxy>> {
|
) -> ApiResult<Json<Proxy>> {
|
||||||
context
|
context.require_permission(None, Permission::ROOT).await?;
|
||||||
.require_permission(None, Permission::ROOT)
|
|
||||||
.await?;
|
|
||||||
Ok(Json(Proxy::get(id.0).await?))
|
Ok(Json(Proxy::get(id.0).await?))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new proxy configuration. Requires root permission.
|
/// Create a new proxy configuration. Requires root permission.
|
||||||
#[oai(path = "/proxy", method = "post", operation_id = "create_proxy")]
|
#[oai(path = "/proxy", method = "post", operation_id = "create_proxy")]
|
||||||
async fn create_proxy(&self, url: PlainText<String>, context: ClientContext) -> ApiResult<()> {
|
async fn create_proxy(&self, url: PlainText<String>, context: ClientContext) -> ApiResult<()> {
|
||||||
context
|
context.require_permission(None, Permission::ROOT).await?;
|
||||||
.require_permission(None, Permission::ROOT)
|
|
||||||
.await?;
|
|
||||||
let entity = Proxy::new(url.0);
|
let entity = Proxy::new(url.0);
|
||||||
Ok(entity.save().await?)
|
Ok(entity.save().await?)
|
||||||
}
|
}
|
||||||
@@ -128,9 +117,7 @@ impl SystemApi {
|
|||||||
url: PlainText<String>,
|
url: PlainText<String>,
|
||||||
context: ClientContext,
|
context: ClientContext,
|
||||||
) -> ApiResult<()> {
|
) -> ApiResult<()> {
|
||||||
context
|
context.require_permission(None, Permission::ROOT).await?;
|
||||||
.require_permission(None, Permission::ROOT)
|
|
||||||
.await?;
|
|
||||||
Ok(Proxy::update(id.0, url.0).await?)
|
Ok(Proxy::update(id.0, url.0).await?)
|
||||||
}
|
}
|
||||||
/// Get system configurations.
|
/// Get system configurations.
|
||||||
@@ -146,9 +133,7 @@ impl SystemApi {
|
|||||||
&self,
|
&self,
|
||||||
context: ClientContext,
|
context: ClientContext,
|
||||||
) -> ApiResult<Json<SystemConfigurations>> {
|
) -> ApiResult<Json<SystemConfigurations>> {
|
||||||
context
|
context.require_permission(None, Permission::ROOT).await?;
|
||||||
.require_permission(None, Permission::ROOT)
|
|
||||||
.await?;
|
|
||||||
let config: SystemConfigurations = SystemConfigurations::from(&*SETTINGS);
|
let config: SystemConfigurations = SystemConfigurations::from(&*SETTINGS);
|
||||||
Ok(Json(config))
|
Ok(Json(config))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,8 +65,7 @@ export function useSidebarData(): SidebarData {
|
|||||||
{
|
{
|
||||||
title: t('navigation.oauth2'),
|
title: t('navigation.oauth2'),
|
||||||
url: '/oauth2',
|
url: '/oauth2',
|
||||||
icon: IdCard,
|
icon: IdCard
|
||||||
visible: require_any_permission(['system:root', 'account:create']),
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ export const getColumns = (t: (key: string) => string): ColumnDef<OAuth2Entity>[
|
|||||||
<DataTableColumnHeader column={column} title={t('settings.id')} />
|
<DataTableColumnHeader column={column} title={t('settings.id')} />
|
||||||
),
|
),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return <LongText className='max-w-[100px]'>{row.original.id}</LongText>
|
return <LongText className='max-w-[140px]'>{row.original.id}</LongText>
|
||||||
},
|
},
|
||||||
meta: { className: 'max-w-[100px]' },
|
meta: { className: 'max-w-[140px]' },
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
@@ -92,9 +92,9 @@ export const getColumns = (t: (key: string) => string): ColumnDef<OAuth2Entity>[
|
|||||||
<DataTableColumnHeader column={column} title={t('settings.description')} />
|
<DataTableColumnHeader column={column} title={t('settings.description')} />
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<LongText className='max-w-[200px]'>{row.original.description}</LongText>
|
<LongText className='max-w-[180px]'>{row.original.description}</LongText>
|
||||||
),
|
),
|
||||||
meta: { className: 'max-w-[200px]' },
|
meta: { className: 'max-w-[180px]' },
|
||||||
enableHiding: true,
|
enableHiding: true,
|
||||||
enableSorting: false
|
enableSorting: false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { useOAuth2Context } from '../context'
|
|||||||
import { OAuth2Entity } from '../data/schema'
|
import { OAuth2Entity } from '../data/schema'
|
||||||
import { WorkflowIcon } from 'lucide-react'
|
import { WorkflowIcon } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useCurrentUser } from '@/hooks/use-current-user'
|
||||||
|
|
||||||
|
|
||||||
interface DataTableRowActionsProps {
|
interface DataTableRowActionsProps {
|
||||||
@@ -42,6 +43,9 @@ interface DataTableRowActionsProps {
|
|||||||
export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { setOpen, setCurrentRow } = useOAuth2Context()
|
const { setOpen, setCurrentRow } = useOAuth2Context()
|
||||||
|
const { require_any_permission } = useCurrentUser()
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
@@ -56,6 +60,7 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align='end' className='w-[160px]'>
|
<DropdownMenuContent align='end' className='w-[160px]'>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
disabled={!require_any_permission(['system:root'])}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCurrentRow(row.original)
|
setCurrentRow(row.original)
|
||||||
setOpen('edit')
|
setOpen('edit')
|
||||||
@@ -80,6 +85,7 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
disabled={!require_any_permission(['system:root'])}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCurrentRow(row.original)
|
setCurrentRow(row.original)
|
||||||
setOpen('delete')
|
setOpen('delete')
|
||||||
|
|||||||
@@ -37,12 +37,13 @@ import { TableSkeleton } from '@/components/table-skeleton'
|
|||||||
import { AuthorizeDialog } from './components/authorize-dialog'
|
import { AuthorizeDialog } from './components/authorize-dialog'
|
||||||
import { FixedHeader } from '@/components/layout/fixed-header'
|
import { FixedHeader } from '@/components/layout/fixed-header'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useCurrentUser } from '@/hooks/use-current-user'
|
||||||
|
|
||||||
export default function OAuth2() {
|
export default function OAuth2() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [currentRow, setCurrentRow] = useState<OAuth2Entity | null>(null)
|
const [currentRow, setCurrentRow] = useState<OAuth2Entity | null>(null)
|
||||||
const [open, setOpen] = useDialogState<OAuth2DialogType>(null)
|
const [open, setOpen] = useDialogState<OAuth2DialogType>(null)
|
||||||
|
const { require_any_permission } = useCurrentUser()
|
||||||
|
|
||||||
const { data: oauth2List, isLoading } = useQuery({
|
const { data: oauth2List, isLoading } = useQuery({
|
||||||
queryKey: ['oauth2-list'],
|
queryKey: ['oauth2-list'],
|
||||||
@@ -53,12 +54,9 @@ export default function OAuth2() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<OAuth2Provider value={{ open, setOpen, currentRow, setCurrentRow }}>
|
<OAuth2Provider value={{ open, setOpen, currentRow, setCurrentRow }}>
|
||||||
{/* ===== Top Heading ===== */}
|
|
||||||
<FixedHeader />
|
<FixedHeader />
|
||||||
|
|
||||||
<Main>
|
<Main>
|
||||||
<div className="mx-auto w-full max-w-6xl px-4">
|
<div className="mx-auto max-w-[88rem] px-4">
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-2 flex items-start flex-wrap gap-x-4 gap-y-2">
|
<div className="mb-2 flex items-start flex-wrap gap-x-4 gap-y-2">
|
||||||
<div className="flex-1 min-w-[300px]">
|
<div className="flex-1 min-w-[300px]">
|
||||||
<h2 className="text-2xl font-bold tracking-tight">{t('oauth2.title')}</h2>
|
<h2 className="text-2xl font-bold tracking-tight">{t('oauth2.title')}</h2>
|
||||||
@@ -67,13 +65,12 @@ export default function OAuth2() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 ml-auto">
|
<div className="flex gap-2 ml-auto">
|
||||||
<Button className="space-x-1" onClick={() => setOpen("add")}>
|
<Button className="space-x-1" disabled={!require_any_permission(['system:root'])} onClick={() => setOpen("add")}>
|
||||||
<span>{t('common.add')}</span>
|
<span>{t('common.add')}</span>
|
||||||
<Plus size={18} />
|
<Plus size={18} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Table / Empty State */}
|
|
||||||
<div className="flex-1 overflow-auto py-1 flex-row lg:space-x-12 space-y-0">
|
<div className="flex-1 overflow-auto py-1 flex-row lg:space-x-12 space-y-0">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<TableSkeleton columns={columns.length} rows={10} />
|
<TableSkeleton columns={columns.length} rows={10} />
|
||||||
@@ -91,7 +88,7 @@ export default function OAuth2() {
|
|||||||
<p className="mb-4 mt-2 text-sm text-muted-foreground">
|
<p className="mb-4 mt-2 text-sm text-muted-foreground">
|
||||||
{t('oauth2.noConfigurationsDesc')}
|
{t('oauth2.noConfigurationsDesc')}
|
||||||
</p>
|
</p>
|
||||||
<Button onClick={() => setOpen("add")}>{t('oauth2.addConfiguration')}</Button>
|
<Button disabled={!require_any_permission(['system:root'])} onClick={() => setOpen("add")}>{t('oauth2.addConfiguration')}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
|
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { require_any_permission, canGlobal } = useCurrentUser()
|
const { canGlobal } = useCurrentUser()
|
||||||
|
|
||||||
|
|
||||||
const sidebarNavItems = [
|
const sidebarNavItems = [
|
||||||
@@ -50,7 +50,6 @@ export default function Settings() {
|
|||||||
title: t('settings.sidebar.proxy'),
|
title: t('settings.sidebar.proxy'),
|
||||||
icon: <Waypoints size={18} />,
|
icon: <Waypoints size={18} />,
|
||||||
href: '/settings/proxy',
|
href: '/settings/proxy',
|
||||||
visible: require_any_permission(['system:root', 'account:create']),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('settings.sidebar.configurations'),
|
title: t('settings.sidebar.configurations'),
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
import { useProxyContext } from '../context'
|
import { useProxyContext } from '../context'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Proxy } from '@/api/system/api'
|
import { Proxy } from '@/api/system/api'
|
||||||
|
import { useCurrentUser } from '@/hooks/use-current-user'
|
||||||
|
|
||||||
|
|
||||||
interface DataTableRowActionsProps {
|
interface DataTableRowActionsProps {
|
||||||
@@ -40,6 +41,7 @@ interface DataTableRowActionsProps {
|
|||||||
|
|
||||||
export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
||||||
const { setOpen, setCurrentRow } = useProxyContext()
|
const { setOpen, setCurrentRow } = useProxyContext()
|
||||||
|
const { require_any_permission } = useCurrentUser()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -55,6 +57,7 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align='end' className='w-[160px]'>
|
<DropdownMenuContent align='end' className='w-[160px]'>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
disabled={!require_any_permission(['system:root'])}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCurrentRow(row.original)
|
setCurrentRow(row.original)
|
||||||
setOpen('edit')
|
setOpen('edit')
|
||||||
@@ -67,6 +70,7 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
disabled={!require_any_permission(['system:root'])}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCurrentRow(row.original)
|
setCurrentRow(row.original)
|
||||||
setOpen('delete')
|
setOpen('delete')
|
||||||
|
|||||||
@@ -33,13 +33,14 @@ import Logo from '@/assets/logo.svg'
|
|||||||
import useProxyList from '@/hooks/use-proxy'
|
import useProxyList from '@/hooks/use-proxy'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Proxy } from '@/api/system/api'
|
import { Proxy } from '@/api/system/api'
|
||||||
|
import { useCurrentUser } from '@/hooks/use-current-user'
|
||||||
|
|
||||||
|
|
||||||
export default function ProxyManagerPage() {
|
export default function ProxyManagerPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [currentRow, setCurrentRow] = useState<Proxy | null>(null)
|
const [currentRow, setCurrentRow] = useState<Proxy | null>(null)
|
||||||
const [open, setOpen] = useDialogState<ProxyDialogType>(null)
|
const [open, setOpen] = useDialogState<ProxyDialogType>(null)
|
||||||
|
const { require_any_permission } = useCurrentUser()
|
||||||
const { proxyList, isLoading } = useProxyList()
|
const { proxyList, isLoading } = useProxyList()
|
||||||
const columns = getColumns(t)
|
const columns = getColumns(t)
|
||||||
|
|
||||||
@@ -49,7 +50,7 @@ export default function ProxyManagerPage() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="mb-4 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2">
|
<div className="mb-4 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button className="space-x-1" onClick={() => setOpen('add')}>
|
<Button className="space-x-1" disabled={!require_any_permission(['system:root'])} onClick={() => setOpen('add')}>
|
||||||
<span>{t('settings.add')}</span> <Plus size={18} />
|
<span>{t('settings.add')}</span> <Plus size={18} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user