mirror of
https://github.com/Buriburizaem0n/admin-frontend-domain.git
synced 2026-02-04 12:40:08 +00:00
User Role (#69)
* fix: window.DisableAnimatedMan as boolean * chore: auto-fix linting and formatting issues * feat: user role * feat: use user agent_secret * feat: hide setting when user role is not admin * feat: new waf api * chore: auto-fix linting and formatting issues * fix: admin settings page * feat: online-user setting * fix: pagination --------- Co-authored-by: hamster1963 <hamster1963@users.noreply.github.com>
This commit is contained in:
@@ -25,7 +25,7 @@
|
|||||||
"@radix-ui/react-scroll-area": "^1.2.1",
|
"@radix-ui/react-scroll-area": "^1.2.1",
|
||||||
"@radix-ui/react-select": "^2.1.2",
|
"@radix-ui/react-select": "^2.1.2",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
"@radix-ui/react-tabs": "^1.1.1",
|
"@radix-ui/react-tabs": "^1.1.1",
|
||||||
"@tanstack/react-table": "^8.20.5",
|
"@tanstack/react-table": "^8.20.5",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
|
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
|
||||||
|
|||||||
5
src/api/online-user.ts
Normal file
5
src/api/online-user.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { FetcherMethod, fetcher } from "./api"
|
||||||
|
|
||||||
|
export const blockUser = async (ip: string[]): Promise<void> => {
|
||||||
|
return fetcher<void>(FetcherMethod.POST, "/api/v1/online-user/batch-block", ip)
|
||||||
|
}
|
||||||
@@ -5,9 +5,10 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
import { useAuth } from "@/hooks/useAuth"
|
||||||
import useSettings from "@/hooks/useSetting"
|
import useSettings from "@/hooks/useSetting"
|
||||||
import { copyToClipboard } from "@/lib/utils"
|
import { copyToClipboard } from "@/lib/utils"
|
||||||
import { ModelSettingResponse } from "@/types"
|
import { ModelProfile, ModelSettingResponse } from "@/types"
|
||||||
import i18next from "i18next"
|
import i18next from "i18next"
|
||||||
import { Check, Clipboard } from "lucide-react"
|
import { Check, Clipboard } from "lucide-react"
|
||||||
import { forwardRef, useState } from "react"
|
import { forwardRef, useState } from "react"
|
||||||
@@ -23,14 +24,17 @@ enum OSTypes {
|
|||||||
export const InstallCommandsMenu = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
|
export const InstallCommandsMenu = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
|
||||||
const [copy, setCopy] = useState(false)
|
const [copy, setCopy] = useState(false)
|
||||||
const { data: settings } = useSettings()
|
const { data: settings } = useSettings()
|
||||||
|
const { profile } = useAuth()
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const switchState = async (type: number) => {
|
const switchState = async (type: number) => {
|
||||||
if (!copy) {
|
if (!copy) {
|
||||||
try {
|
try {
|
||||||
setCopy(true)
|
setCopy(true)
|
||||||
|
if (!profile) throw new Error("Profile is not found.")
|
||||||
if (!settings) throw new Error("Settings is not found.")
|
if (!settings) throw new Error("Settings is not found.")
|
||||||
await copyToClipboard(generateCommand(type, settings) || "")
|
await copyToClipboard(generateCommand(type, settings, profile) || "")
|
||||||
} catch (e: Error | any) {
|
} catch (e: Error | any) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
toast(t("Error"), {
|
toast(t("Error"), {
|
||||||
@@ -85,9 +89,19 @@ export const InstallCommandsMenu = forwardRef<HTMLButtonElement, ButtonProps>((p
|
|||||||
const generateCommand = (
|
const generateCommand = (
|
||||||
type: number,
|
type: number,
|
||||||
{ agent_secret_key, install_host, tls }: ModelSettingResponse,
|
{ agent_secret_key, install_host, tls }: ModelSettingResponse,
|
||||||
|
{ agent_secret, role }: ModelProfile,
|
||||||
) => {
|
) => {
|
||||||
if (!install_host) throw new Error(i18next.t("Results.InstallHostRequired"))
|
if (!install_host) throw new Error(i18next.t("Results.InstallHostRequired"))
|
||||||
|
|
||||||
|
// 如果 agent_secret 为空且 role 为 0 ,则使用 agent_secret_key,否则如果 agent_secret 为空则报错
|
||||||
|
if (!agent_secret && role === 0) {
|
||||||
|
agent_secret = agent_secret_key
|
||||||
|
} else if (!agent_secret) {
|
||||||
|
throw new Error(i18next.t("Results.AgentSecretRequired"))
|
||||||
|
}
|
||||||
|
|
||||||
|
agent_secret_key = agent_secret
|
||||||
|
|
||||||
const env = `NZ_SERVER=${install_host} NZ_TLS=${tls || false} NZ_CLIENT_SECRET=${agent_secret_key}`
|
const env = `NZ_SERVER=${install_host} NZ_TLS=${tls || false} NZ_CLIENT_SECRET=${agent_secret_key}`
|
||||||
const env_win = `$env:NZ_SERVER=\"${install_host}\";$env:NZ_TLS=\"${tls || false}\";$env:NZ_CLIENT_SECRET=\"${agent_secret_key}\";`
|
const env_win = `$env:NZ_SERVER=\"${install_host}\";$env:NZ_TLS=\"${tls || false}\";$env:NZ_CLIENT_SECRET=\"${agent_secret_key}\";`
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,30 @@
|
|||||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
|
import { useAuth } from "@/hooks/useAuth"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link, useLocation } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
|
|
||||||
export const SettingsTab = ({ className }: { className?: string }) => {
|
export const SettingsTab = ({ className }: { className?: string }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const location = useLocation()
|
const { profile } = useAuth()
|
||||||
|
|
||||||
|
const isAdmin = profile?.role === 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs defaultValue={location.pathname} className={className}>
|
<Tabs defaultValue={window.location.pathname} className={className}>
|
||||||
<TabsList className="grid w-full grid-cols-3">
|
<TabsList className="grid w-full grid-cols-4">
|
||||||
<TabsTrigger value="/dashboard/settings" asChild>
|
{isAdmin && (
|
||||||
<Link to="/dashboard/settings">{t("Settings")}</Link>
|
<>
|
||||||
</TabsTrigger>
|
<TabsTrigger value="/dashboard/settings" asChild>
|
||||||
<TabsTrigger value="/dashboard/settings/user" asChild>
|
<Link to="/dashboard/settings">{t("Settings")}</Link>
|
||||||
<Link to="/dashboard/settings/user">{t("User")}</Link>
|
</TabsTrigger>
|
||||||
</TabsTrigger>
|
<TabsTrigger value="/dashboard/settings/user" asChild>
|
||||||
|
<Link to="/dashboard/settings/user">{t("User")}</Link>
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="/dashboard/settings/online-user" asChild>
|
||||||
|
<Link to="/dashboard/settings/online-user">{t("OnlineUser")}</Link>
|
||||||
|
</TabsTrigger>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<TabsTrigger value="/dashboard/settings/waf" asChild>
|
<TabsTrigger value="/dashboard/settings/waf" asChild>
|
||||||
<Link to="/dashboard/settings/waf">{t("WAF")}</Link>
|
<Link to="/dashboard/settings/waf">{t("WAF")}</Link>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
|||||||
97
src/components/ui/pagination.tsx
Normal file
97
src/components/ui/pagination.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { ButtonProps, buttonVariants } from "@/components/ui/button"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
|
||||||
|
<nav
|
||||||
|
role="navigation"
|
||||||
|
aria-label="pagination"
|
||||||
|
className={cn("mx-auto flex w-full justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
Pagination.displayName = "Pagination"
|
||||||
|
|
||||||
|
const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<ul ref={ref} className={cn("flex flex-row items-center gap-1", className)} {...props} />
|
||||||
|
),
|
||||||
|
)
|
||||||
|
PaginationContent.displayName = "PaginationContent"
|
||||||
|
|
||||||
|
const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(
|
||||||
|
({ className, ...props }, ref) => <li ref={ref} className={cn("", className)} {...props} />,
|
||||||
|
)
|
||||||
|
PaginationItem.displayName = "PaginationItem"
|
||||||
|
|
||||||
|
type PaginationLinkProps = {
|
||||||
|
isActive?: boolean
|
||||||
|
} & Pick<ButtonProps, "size"> &
|
||||||
|
React.ComponentProps<"a">
|
||||||
|
|
||||||
|
const PaginationLink = ({ className, isActive, size = "icon", ...props }: PaginationLinkProps) => (
|
||||||
|
<a
|
||||||
|
aria-current={isActive ? "page" : undefined}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: isActive ? "outline" : "ghost",
|
||||||
|
size,
|
||||||
|
}),
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
PaginationLink.displayName = "PaginationLink"
|
||||||
|
|
||||||
|
const PaginationPrevious = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Go to previous page"
|
||||||
|
size="default"
|
||||||
|
className={cn("gap-1 pl-2.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
<span>Previous</span>
|
||||||
|
</PaginationLink>
|
||||||
|
)
|
||||||
|
PaginationPrevious.displayName = "PaginationPrevious"
|
||||||
|
|
||||||
|
const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Go to next page"
|
||||||
|
size="default"
|
||||||
|
className={cn("gap-1 pr-2.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span>Next</span>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</PaginationLink>
|
||||||
|
)
|
||||||
|
PaginationNext.displayName = "PaginationNext"
|
||||||
|
|
||||||
|
const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
<span className="sr-only">More pages</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
PaginationEllipsis.displayName = "PaginationEllipsis"
|
||||||
|
|
||||||
|
export {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationEllipsis,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrevious,
|
||||||
|
}
|
||||||
@@ -20,6 +20,13 @@ import {
|
|||||||
} from "@/components/ui/form"
|
} from "@/components/ui/form"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select"
|
||||||
import { IconButton } from "@/components/xui/icon-button"
|
import { IconButton } from "@/components/xui/icon-button"
|
||||||
import { ModelUser } from "@/types"
|
import { ModelUser } from "@/types"
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
@@ -35,6 +42,7 @@ interface UserCardProps {
|
|||||||
|
|
||||||
const userFormSchema = z.object({
|
const userFormSchema = z.object({
|
||||||
username: z.string().min(1),
|
username: z.string().min(1),
|
||||||
|
role: z.number().int().min(0).max(1),
|
||||||
password: z.string().min(8).max(72),
|
password: z.string().min(8).max(72),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -44,6 +52,7 @@ export const UserCard: React.FC<UserCardProps> = ({ mutate }) => {
|
|||||||
resolver: zodResolver(userFormSchema),
|
resolver: zodResolver(userFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: "",
|
username: "",
|
||||||
|
role: 1,
|
||||||
password: "",
|
password: "",
|
||||||
},
|
},
|
||||||
resetOptions: {
|
resetOptions: {
|
||||||
@@ -100,6 +109,34 @@ export const UserCard: React.FC<UserCardProps> = ({ mutate }) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="role"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("Role")}</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) =>
|
||||||
|
field.onChange(parseInt(value))
|
||||||
|
}
|
||||||
|
defaultValue={field.value.toString()}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue
|
||||||
|
placeholder={t("SelectRole")}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="0">{t("Admin")}</SelectItem>
|
||||||
|
<SelectItem value="1">{t("User")}</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<DialogFooter className="justify-end">
|
<DialogFooter className="justify-end">
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button type="button" className="my-2" variant="secondary">
|
<Button type="button" className="my-2" variant="secondary">
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
;(async () => {
|
;(async () => {
|
||||||
try {
|
try {
|
||||||
const user = await getProfile()
|
const user = await getProfile()
|
||||||
|
user.role = user.role || 0
|
||||||
setProfile(user)
|
setProfile(user)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
setProfile(undefined)
|
setProfile(undefined)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import LoginPage from "./routes/login"
|
|||||||
import NATPage from "./routes/nat"
|
import NATPage from "./routes/nat"
|
||||||
import NotificationPage from "./routes/notification"
|
import NotificationPage from "./routes/notification"
|
||||||
import NotificationGroupPage from "./routes/notification-group"
|
import NotificationGroupPage from "./routes/notification-group"
|
||||||
|
import OnlineUserPage from "./routes/online-user"
|
||||||
import ProfilePage from "./routes/profile"
|
import ProfilePage from "./routes/profile"
|
||||||
import ProtectedRoute from "./routes/protect"
|
import ProtectedRoute from "./routes/protect"
|
||||||
import Root from "./routes/root"
|
import Root from "./routes/root"
|
||||||
@@ -133,6 +134,10 @@ const router = createBrowserRouter([
|
|||||||
path: "/dashboard/settings/waf",
|
path: "/dashboard/settings/waf",
|
||||||
element: <WAFPage />,
|
element: <WAFPage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/dashboard/settings/online-user",
|
||||||
|
element: <OnlineUserPage />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
305
src/routes/online-user.tsx
Normal file
305
src/routes/online-user.tsx
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
import { swrFetcher } from "@/api/api"
|
||||||
|
import { blockUser } from "@/api/online-user"
|
||||||
|
import { ActionButtonGroup } from "@/components/action-button-group"
|
||||||
|
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||||
|
import { SettingsTab } from "@/components/settings-tab"
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
|
import {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationEllipsis,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrevious,
|
||||||
|
} from "@/components/ui/pagination"
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table"
|
||||||
|
import { useAuth } from "@/hooks/useAuth"
|
||||||
|
import { ModelOnlineUser, ModelOnlineUserApi } from "@/types"
|
||||||
|
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||||
|
import { useEffect, useMemo } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useSearchParams } from "react-router-dom"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
import useSWR from "swr"
|
||||||
|
|
||||||
|
export default function OnlineUserPage() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { profile } = useAuth()
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
const page = Number(searchParams.get("page")) || 1
|
||||||
|
const pageSize = Number(searchParams.get("pageSize")) || 10
|
||||||
|
|
||||||
|
// 计算 offset
|
||||||
|
const offset = (page - 1) * pageSize
|
||||||
|
|
||||||
|
const { data, mutate, error, isLoading } = useSWR<ModelOnlineUserApi>(
|
||||||
|
`/api/v1/online-user?offset=${offset}&limit=${pageSize}`,
|
||||||
|
swrFetcher,
|
||||||
|
)
|
||||||
|
|
||||||
|
const isAdmin = profile?.role === 0
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (error)
|
||||||
|
toast(t("Error"), {
|
||||||
|
description: t(`Error fetching resource: ${error.message}.`),
|
||||||
|
})
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [error])
|
||||||
|
|
||||||
|
let columns: ColumnDef<ModelOnlineUser>[] = [
|
||||||
|
{
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => (
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
table.getIsAllPageRowsSelected() ||
|
||||||
|
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||||
|
}
|
||||||
|
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||||
|
aria-label="Select all"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Checkbox
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
aria-label="Select row"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
enableHiding: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "IP",
|
||||||
|
accessorKey: "ip",
|
||||||
|
accessorFn: (row) => row.ip ?? "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: t("UserId"),
|
||||||
|
accessorKey: "user_id",
|
||||||
|
accessorFn: (row) => row.user_id || "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: t("ConnectedAt"),
|
||||||
|
accessorKey: "connected_at",
|
||||||
|
accessorFn: (row) => row.connected_at,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const s = row.original
|
||||||
|
const date = new Date(s.connected_at)
|
||||||
|
return <span>{date.toISOString()}</span>
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
header: "Actions",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const s = row.original
|
||||||
|
return (
|
||||||
|
<ActionButtonGroup
|
||||||
|
className="flex gap-2"
|
||||||
|
delete={{
|
||||||
|
fn: blockUser,
|
||||||
|
id: s.ip ?? "",
|
||||||
|
mutate: mutate,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<></>
|
||||||
|
</ActionButtonGroup>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
// 非管理员隐藏操作列
|
||||||
|
columns = columns.filter((c) => c.id !== "actions")
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataCache = useMemo(() => {
|
||||||
|
return data?.value ?? []
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: dataCache,
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedRows = table.getSelectedRowModel().rows
|
||||||
|
|
||||||
|
const renderPagination = () => {
|
||||||
|
if (!data?.pagination) return null
|
||||||
|
|
||||||
|
const { total } = data.pagination
|
||||||
|
const totalPages = Math.ceil(total / pageSize)
|
||||||
|
|
||||||
|
const handlePageChange = (newPage: number) => {
|
||||||
|
if (newPage < 1 || newPage > totalPages) return
|
||||||
|
setSearchParams({ page: newPage.toString(), pageSize: pageSize.toString() })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算要显示的页码范围
|
||||||
|
const getPageNumbers = () => {
|
||||||
|
const pages: number[] = []
|
||||||
|
const maxVisiblePages = 5
|
||||||
|
|
||||||
|
if (totalPages <= maxVisiblePages) {
|
||||||
|
return Array.from({ length: totalPages }, (_, i) => i + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 始终显示第一页
|
||||||
|
pages.push(1)
|
||||||
|
|
||||||
|
let startPage = Math.max(2, page - 1)
|
||||||
|
let endPage = Math.min(totalPages - 1, page + 1)
|
||||||
|
|
||||||
|
if (page <= 3) {
|
||||||
|
endPage = Math.min(maxVisiblePages - 1, totalPages - 1)
|
||||||
|
} else if (page >= totalPages - 2) {
|
||||||
|
startPage = Math.max(2, totalPages - (maxVisiblePages - 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startPage > 2) {
|
||||||
|
pages.push(-1) // 表示省略号
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
pages.push(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endPage < totalPages - 1) {
|
||||||
|
pages.push(-1) // 表示省略号
|
||||||
|
}
|
||||||
|
|
||||||
|
// 始终显示最后一页
|
||||||
|
if (totalPages > 1) {
|
||||||
|
pages.push(totalPages)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between px-2 py-4">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{t("Total")}: {total}
|
||||||
|
</div>
|
||||||
|
<Pagination>
|
||||||
|
<PaginationContent>
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationPrevious
|
||||||
|
onClick={() => handlePageChange(page - 1)}
|
||||||
|
className={
|
||||||
|
page <= 1 ? "pointer-events-none opacity-50" : "cursor-pointer"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
|
||||||
|
{getPageNumbers().map((pageNum, idx) =>
|
||||||
|
pageNum === -1 ? (
|
||||||
|
<PaginationItem key={`ellipsis-${idx}`}>
|
||||||
|
<PaginationEllipsis />
|
||||||
|
</PaginationItem>
|
||||||
|
) : (
|
||||||
|
<PaginationItem key={pageNum}>
|
||||||
|
<PaginationLink
|
||||||
|
onClick={() => handlePageChange(pageNum)}
|
||||||
|
isActive={pageNum === page}
|
||||||
|
>
|
||||||
|
{pageNum}
|
||||||
|
</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationNext
|
||||||
|
onClick={() => handlePageChange(page + 1)}
|
||||||
|
className={
|
||||||
|
page >= totalPages
|
||||||
|
? "pointer-events-none opacity-50"
|
||||||
|
: "cursor-pointer"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="px-3">
|
||||||
|
<SettingsTab className="mt-6 w-full" />
|
||||||
|
<div className="flex mt-4 mb-4">
|
||||||
|
{isAdmin && (
|
||||||
|
<HeaderButtonGroup
|
||||||
|
className="flex-2 flex gap-2 ml-auto"
|
||||||
|
delete={{
|
||||||
|
fn: blockUser,
|
||||||
|
id: selectedRows.map((r) => r.original.ip ?? ""),
|
||||||
|
mutate: mutate,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<></>
|
||||||
|
</HeaderButtonGroup>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead key={header.id} className="text-sm">
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{isLoading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
|
{t("Loading")}...
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id} className="text-xsm">
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
|
{t("NoResults")}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
{renderPagination()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select"
|
} from "@/components/ui/select"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
import { useAuth } from "@/hooks/useAuth"
|
||||||
import useSetting from "@/hooks/useSetting"
|
import useSetting from "@/hooks/useSetting"
|
||||||
import { asOptionalField } from "@/lib/utils"
|
import { asOptionalField } from "@/lib/utils"
|
||||||
import { nezhaLang, settingCoverageTypes } from "@/types"
|
import { nezhaLang, settingCoverageTypes } from "@/types"
|
||||||
@@ -28,6 +29,7 @@ import { zodResolver } from "@hookform/resolvers/zod"
|
|||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
@@ -52,6 +54,17 @@ const settingFormSchema = z.object({
|
|||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const { t, i18n } = useTranslation()
|
const { t, i18n } = useTranslation()
|
||||||
const { data: config, mutate } = useSetting()
|
const { data: config, mutate } = useSetting()
|
||||||
|
const { profile } = useAuth()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const isAdmin = profile?.role === 0
|
||||||
|
|
||||||
|
console.log(isAdmin)
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
console.log("redirect")
|
||||||
|
navigate("/dashboard/settings/waf")
|
||||||
|
}
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof settingFormSchema>>({
|
const form = useForm<z.infer<typeof settingFormSchema>>({
|
||||||
resolver: zodResolver(settingFormSchema),
|
resolver: zodResolver(settingFormSchema),
|
||||||
@@ -62,7 +75,7 @@ export default function SettingsPage() {
|
|||||||
site_name: config.site_name || "",
|
site_name: config.site_name || "",
|
||||||
user_template:
|
user_template:
|
||||||
config.user_template ||
|
config.user_template ||
|
||||||
Object.keys(config.frontend_templates.filter((t) => !t.is_admin) || {})[0] ||
|
Object.keys(config.frontend_templates?.filter((t) => !t.is_admin) || {})[0] ||
|
||||||
"user-dist",
|
"user-dist",
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
@@ -173,7 +186,7 @@ export default function SettingsPage() {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{(
|
{(
|
||||||
config?.frontend_templates.filter(
|
config?.frontend_templates?.filter(
|
||||||
(t) => !t.is_admin,
|
(t) => !t.is_admin,
|
||||||
) || []
|
) || []
|
||||||
).map((template) => (
|
).map((template) => (
|
||||||
|
|||||||
@@ -67,6 +67,13 @@ export default function UserPage() {
|
|||||||
accessorKey: "username",
|
accessorKey: "username",
|
||||||
accessorFn: (row) => row.username,
|
accessorFn: (row) => row.username,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
header: t("Role"),
|
||||||
|
accessorKey: "role",
|
||||||
|
accessorFn: (row) => {
|
||||||
|
return row.role === 1 ? t("User") : t("Admin")
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
header: t("Actions"),
|
header: t("Actions"),
|
||||||
|
|||||||
@@ -4,6 +4,15 @@ import { ActionButtonGroup } from "@/components/action-button-group"
|
|||||||
import { HeaderButtonGroup } from "@/components/header-button-group"
|
import { HeaderButtonGroup } from "@/components/header-button-group"
|
||||||
import { SettingsTab } from "@/components/settings-tab"
|
import { SettingsTab } from "@/components/settings-tab"
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
|
import {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationEllipsis,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrevious,
|
||||||
|
} from "@/components/ui/pagination"
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -12,17 +21,32 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table"
|
} from "@/components/ui/table"
|
||||||
|
import { useAuth } from "@/hooks/useAuth"
|
||||||
import { ip16Str } from "@/lib/utils"
|
import { ip16Str } from "@/lib/utils"
|
||||||
import { ModelWAFApiMock, wafBlockReasons } from "@/types"
|
import { ModelWAF, ModelWAFApiMock, wafBlockIdentifiers, wafBlockReasons } from "@/types"
|
||||||
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
|
||||||
import { useEffect, useMemo } from "react"
|
import { useEffect, useMemo } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useSearchParams } from "react-router-dom"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import useSWR from "swr"
|
import useSWR from "swr"
|
||||||
|
|
||||||
export default function WAFPage() {
|
export default function WAFPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { data, mutate, error, isLoading } = useSWR<ModelWAFApiMock[]>("/api/v1/waf", swrFetcher)
|
const { profile } = useAuth()
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
const page = Number(searchParams.get("page")) || 1
|
||||||
|
const pageSize = Number(searchParams.get("pageSize")) || 10
|
||||||
|
|
||||||
|
// 计算 offset
|
||||||
|
const offset = (page - 1) * pageSize
|
||||||
|
|
||||||
|
const { data, mutate, error, isLoading } = useSWR<ModelWAFApiMock>(
|
||||||
|
`/api/v1/waf?offset=${offset}&limit=${pageSize}`,
|
||||||
|
swrFetcher,
|
||||||
|
)
|
||||||
|
|
||||||
|
const isAdmin = profile?.role === 0
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error)
|
if (error)
|
||||||
@@ -32,7 +56,7 @@ export default function WAFPage() {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [error])
|
}, [error])
|
||||||
|
|
||||||
const columns: ColumnDef<ModelWAFApiMock>[] = [
|
let columns: ColumnDef<ModelWAF>[] = [
|
||||||
{
|
{
|
||||||
id: "select",
|
id: "select",
|
||||||
header: ({ table }) => (
|
header: ({ table }) => (
|
||||||
@@ -68,16 +92,23 @@ export default function WAFPage() {
|
|||||||
{
|
{
|
||||||
header: t("LastBlockReason"),
|
header: t("LastBlockReason"),
|
||||||
accessorKey: "lastBlockReason",
|
accessorKey: "lastBlockReason",
|
||||||
accessorFn: (row) => row.last_block_reason,
|
accessorFn: (row) => row.block_reason,
|
||||||
cell: ({ row }) => <span>{wafBlockReasons[row.original.last_block_reason] || ""}</span>,
|
cell: ({ row }) => <span>{wafBlockReasons[row.original.block_reason] || ""}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: t("LastBlockIdentifier"),
|
||||||
|
accessorKey: "lastBlockIdentifier",
|
||||||
|
accessorFn: (row) => (
|
||||||
|
<span>{wafBlockIdentifiers[row.block_identifier] || row.block_identifier}</span>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: t("LastBlockTime"),
|
header: t("LastBlockTime"),
|
||||||
accessorKey: "lastBlockTime",
|
accessorKey: "lastBlockTime",
|
||||||
accessorFn: (row) => row.last_block_timestamp,
|
accessorFn: (row) => row.block_timestamp,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const s = row.original
|
const s = row.original
|
||||||
const date = new Date((s.last_block_timestamp || 0) * 1000)
|
const date = new Date((s.block_timestamp || 0) * 1000)
|
||||||
return <span>{date.toISOString()}</span>
|
return <span>{date.toISOString()}</span>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -102,8 +133,13 @@ export default function WAFPage() {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
// 非管理员隐藏操作列
|
||||||
|
columns = columns.filter((c) => c.id !== "actions")
|
||||||
|
}
|
||||||
|
|
||||||
const dataCache = useMemo(() => {
|
const dataCache = useMemo(() => {
|
||||||
return data ?? []
|
return data?.value ?? []
|
||||||
}, [data])
|
}, [data])
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
@@ -114,20 +150,123 @@ export default function WAFPage() {
|
|||||||
|
|
||||||
const selectedRows = table.getSelectedRowModel().rows
|
const selectedRows = table.getSelectedRowModel().rows
|
||||||
|
|
||||||
|
const renderPagination = () => {
|
||||||
|
if (!data?.pagination) return null
|
||||||
|
|
||||||
|
const { total } = data.pagination
|
||||||
|
const totalPages = Math.ceil(total / pageSize)
|
||||||
|
|
||||||
|
const handlePageChange = (newPage: number) => {
|
||||||
|
if (newPage < 1 || newPage > totalPages) return
|
||||||
|
setSearchParams({ page: newPage.toString(), pageSize: pageSize.toString() })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算要显示的页码范围
|
||||||
|
const getPageNumbers = () => {
|
||||||
|
const pages: number[] = []
|
||||||
|
const maxVisiblePages = 5
|
||||||
|
|
||||||
|
if (totalPages <= maxVisiblePages) {
|
||||||
|
return Array.from({ length: totalPages }, (_, i) => i + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 始终显示第一页
|
||||||
|
pages.push(1)
|
||||||
|
|
||||||
|
let startPage = Math.max(2, page - 1)
|
||||||
|
let endPage = Math.min(totalPages - 1, page + 1)
|
||||||
|
|
||||||
|
if (page <= 3) {
|
||||||
|
endPage = Math.min(maxVisiblePages - 1, totalPages - 1)
|
||||||
|
} else if (page >= totalPages - 2) {
|
||||||
|
startPage = Math.max(2, totalPages - (maxVisiblePages - 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startPage > 2) {
|
||||||
|
pages.push(-1) // 表示省略号
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
pages.push(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endPage < totalPages - 1) {
|
||||||
|
pages.push(-1) // 表示省略号
|
||||||
|
}
|
||||||
|
|
||||||
|
// 始终显示最后一页
|
||||||
|
if (totalPages > 1) {
|
||||||
|
pages.push(totalPages)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between px-2 py-4">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{t("Total")}: {total}
|
||||||
|
</div>
|
||||||
|
<Pagination>
|
||||||
|
<PaginationContent>
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationPrevious
|
||||||
|
onClick={() => handlePageChange(page - 1)}
|
||||||
|
className={
|
||||||
|
page <= 1 ? "pointer-events-none opacity-50" : "cursor-pointer"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
|
||||||
|
{getPageNumbers().map((pageNum, idx) =>
|
||||||
|
pageNum === -1 ? (
|
||||||
|
<PaginationItem key={`ellipsis-${idx}`}>
|
||||||
|
<PaginationEllipsis />
|
||||||
|
</PaginationItem>
|
||||||
|
) : (
|
||||||
|
<PaginationItem key={pageNum}>
|
||||||
|
<PaginationLink
|
||||||
|
onClick={() => handlePageChange(pageNum)}
|
||||||
|
isActive={pageNum === page}
|
||||||
|
>
|
||||||
|
{pageNum}
|
||||||
|
</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationNext
|
||||||
|
onClick={() => handlePageChange(page + 1)}
|
||||||
|
className={
|
||||||
|
page >= totalPages
|
||||||
|
? "pointer-events-none opacity-50"
|
||||||
|
: "cursor-pointer"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-3">
|
<div className="px-3">
|
||||||
<SettingsTab className="mt-6 w-full" />
|
<SettingsTab className="mt-6 w-full" />
|
||||||
<div className="flex mt-4 mb-4">
|
<div className="flex mt-4 mb-4">
|
||||||
<HeaderButtonGroup
|
{isAdmin && (
|
||||||
className="flex-2 flex gap-2 ml-auto"
|
<HeaderButtonGroup
|
||||||
delete={{
|
className="flex-2 flex gap-2 ml-auto"
|
||||||
fn: deleteWAF,
|
delete={{
|
||||||
id: selectedRows.map((r) => ip16Str(r.original.ip ?? "")),
|
fn: deleteWAF,
|
||||||
mutate: mutate,
|
id: selectedRows.map((r) => ip16Str(r.original.ip ?? "")),
|
||||||
}}
|
mutate: mutate,
|
||||||
>
|
}}
|
||||||
<></>
|
>
|
||||||
</HeaderButtonGroup>
|
<></>
|
||||||
|
</HeaderButtonGroup>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
@@ -174,6 +313,7 @@ export default function WAFPage() {
|
|||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
{renderPagination()}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -404,6 +404,8 @@ export interface ModelProfile {
|
|||||||
password: string
|
password: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
username: string
|
username: string
|
||||||
|
role: number
|
||||||
|
agent_secret: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModelProfileForm {
|
export interface ModelProfileForm {
|
||||||
@@ -673,6 +675,7 @@ export interface ModelUser {
|
|||||||
password: string
|
password: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
username: string
|
username: string
|
||||||
|
role: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModelUserForm {
|
export interface ModelUserForm {
|
||||||
@@ -680,9 +683,32 @@ export interface ModelUserForm {
|
|||||||
username: string
|
username: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModelWAFApiMock {
|
export interface Pagination {
|
||||||
|
limit: number
|
||||||
|
offset: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelWAF {
|
||||||
count: number
|
count: number
|
||||||
ip: string
|
ip: string
|
||||||
last_block_reason: number
|
block_identifier: number
|
||||||
last_block_timestamp: number
|
block_reason: number
|
||||||
|
block_timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelWAFApiMock {
|
||||||
|
pagination: Pagination
|
||||||
|
value: ModelWAF[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelOnlineUser {
|
||||||
|
connected_at: string
|
||||||
|
ip: string
|
||||||
|
user_id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelOnlineUserApi {
|
||||||
|
pagination: Pagination
|
||||||
|
value: ModelOnlineUser[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,3 +16,10 @@ export const wafBlockReasons: Record<number, string> = {
|
|||||||
2: i18n.t("BruteForceAttackingToken"),
|
2: i18n.t("BruteForceAttackingToken"),
|
||||||
3: i18n.t("BruteForceAttackingAgentSecret"),
|
3: i18n.t("BruteForceAttackingAgentSecret"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const wafBlockIdentifiers: Record<number, string> = {
|
||||||
|
"-127": i18n.t("GrpcAuthFailed"),
|
||||||
|
"-126": i18n.t("APITokenInvalid"),
|
||||||
|
"-125": i18n.t("UserInvalid"),
|
||||||
|
"-124": i18n.t("BlockByUser"),
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user