├── .gitignore ├── README-zh_CN.md ├── README.md ├── components.json ├── components ├── theme-provider.tsx └── ui │ ├── button.tsx │ ├── card.tsx │ ├── chart.tsx │ ├── input-otp.tsx │ ├── resizable.tsx │ ├── scroll-area.tsx │ ├── separator.tsx │ ├── skeleton.tsx │ └── table.tsx ├── index.html ├── lib └── utils.ts ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── logo.png └── logo.svg ├── screenshots ├── en │ ├── access.png │ └── dashboard.png └── zh │ ├── access.png │ └── dashboard.png ├── src ├── App.css ├── App.tsx ├── api │ ├── access-setting-api.ts │ ├── account-api.ts │ ├── agent-gateway-api.ts │ ├── agent-gateway-token-api.ts │ ├── anonymous-api.ts │ ├── asset-api.ts │ ├── authorised-asset-api.ts │ ├── authorised-website-api.ts │ ├── branding-api.ts │ ├── certificate-api.ts │ ├── command-filter-api.ts │ ├── command-filter-rule-api.ts │ ├── core │ │ ├── api.ts │ │ ├── event-emitter.ts │ │ └── requests.ts │ ├── credential-api.ts │ ├── dashboard-api.ts │ ├── dns-provider-api.ts │ ├── filesystem-api.ts │ ├── fileystem-log-api.ts │ ├── license-api.ts │ ├── login-locked-api.ts │ ├── login-log-api.ts │ ├── login-policy-api.ts │ ├── logo-api.ts │ ├── operation-log-api.ts │ ├── portal-api.ts │ ├── property-api.ts │ ├── role-api.ts │ ├── scheduled-task-api.ts │ ├── session-api.ts │ ├── session-command-api.ts │ ├── snippet-api.ts │ ├── snippet-user-api.ts │ ├── ssh-gateway-api.ts │ ├── storage-api.ts │ ├── strategy-api.ts │ ├── user-api.ts │ ├── user-group-api.ts │ └── website-api.ts ├── assets │ ├── images │ │ ├── linux.png │ │ ├── macos.png │ │ └── windows.png │ └── os │ │ ├── alinux.png │ │ ├── aliyun.png │ │ ├── amzn.png │ │ ├── apple.png │ │ ├── arch.png │ │ ├── bytebase.png │ │ ├── centos.png │ │ ├── cloudcone.png │ │ ├── cloudflare.png │ │ ├── cmcc.png │ │ ├── ct.png │ │ ├── ctyun.png │ │ ├── cu.png │ │ ├── darwin.png │ │ ├── debian.png │ │ ├── dsm.png │ │ ├── esxi.png │ │ ├── fedora.png │ │ ├── gcloud.png │ │ ├── gcore.png │ │ ├── ggy-icon.png │ │ ├── ggy.png │ │ ├── kali.png │ │ ├── linux.png │ │ ├── opencloud.png │ │ ├── opensuse.png │ │ ├── openwrt.png │ │ ├── pfsense.png │ │ ├── pve.png │ │ ├── qcloud.png │ │ ├── qnap.png │ │ ├── redhat.png │ │ ├── rocky.png │ │ ├── synology.png │ │ ├── ubuntu.png │ │ ├── ucloud.png │ │ ├── unraid.png │ │ ├── vm.png │ │ ├── vmb.png │ │ ├── vmw.png │ │ ├── vultr.png │ │ ├── vyos.png │ │ ├── win11.png │ │ ├── win7.png │ │ ├── windows.png │ │ └── zenlayer.png ├── beautiful-scrollbar.css ├── color-theme │ └── XtermThemes.ts ├── components │ ├── CpuProgressBar.tsx │ ├── Disabled.tsx │ ├── ErrorPage.tsx │ ├── FloatingButton.tsx │ ├── Landing.tsx │ ├── NButton.tsx │ ├── NLink.tsx │ ├── NoMatch.tsx │ ├── PromptModal.tsx │ ├── Timeout.tsx │ ├── charts │ │ ├── ConnChart.tsx │ │ ├── CpuChart.tsx │ │ ├── DiskIOChart.tsx │ │ ├── MemoryChart.tsx │ │ ├── NetIOChart.tsx │ │ └── StateChart.tsx │ ├── drag-weektime │ │ ├── DragWeekTime.css │ │ └── DragWeekTime.tsx │ └── time │ │ └── times.ts ├── helper │ ├── access-tab-channel.ts │ └── asset-helper.ts ├── hook │ ├── atom.ts │ ├── title.ts │ ├── use-access-setting.ts │ ├── use-access-size.ts │ ├── use-access-tab.ts │ ├── use-filesystem-id.ts │ ├── use-guacamole.ts │ ├── use-lang.ts │ ├── use-license.ts │ ├── use-terminal-theme.ts │ ├── use-theme.ts │ └── use-window-focus.ts ├── index.css ├── layout │ ├── FooterComponent.tsx │ ├── ManagerLayout.css │ ├── ManagerLayout.tsx │ ├── RedirectPage.tsx │ ├── UserHeader.tsx │ ├── UserLayout.css │ ├── UserLayout.tsx │ └── menus.tsx ├── main.tsx ├── pages │ ├── access │ │ ├── AccessGuacamole.tsx │ │ ├── AccessPage.css │ │ ├── AccessPage.tsx │ │ ├── AccessSetting.tsx │ │ ├── AccessSshChooser.tsx │ │ ├── AccessStats.tsx │ │ ├── AccessTerminal.tsx │ │ ├── AccessTerminalBulk.tsx │ │ ├── AccessTerminalBulkItem.tsx │ │ ├── AccessTheme.tsx │ │ ├── BrowserPage.css │ │ ├── BrowserPage.tsx │ │ ├── FileSystemPage.tsx │ │ ├── GuacClipboard.tsx │ │ ├── GuacamolePage.tsx │ │ ├── GuacdMonitor.tsx │ │ ├── GuacdPlayback.css │ │ ├── GuacdPlayback.tsx │ │ ├── GuacdRequiredParameters.tsx │ │ ├── MobileAccessTerminal.tsx │ │ ├── SessionSharerModal.tsx │ │ ├── SnippetSheet.tsx │ │ ├── Terminal.ts │ │ ├── TerminalMonitor.tsx │ │ ├── TerminalPage.tsx │ │ ├── TerminalPlayback.css │ │ ├── TerminalPlayback.tsx │ │ └── guacamole │ │ │ ├── ControlButtons.tsx │ │ │ ├── ErrorAlert.tsx │ │ │ ├── RenderState.tsx │ │ │ └── keys.ts │ ├── account │ │ ├── AccessToken.tsx │ │ ├── ChangeInfo.tsx │ │ ├── ChangePassword.tsx │ │ ├── InfoPage.tsx │ │ ├── LoginPage.tsx │ │ ├── MultiFactorAuthentication.tsx │ │ ├── OTP.tsx │ │ ├── OTPBinding.tsx │ │ ├── OTPUnBinding.tsx │ │ ├── Passkey.tsx │ │ └── PasskeyModal.tsx │ ├── assets │ │ ├── AssetDetail.tsx │ │ ├── AssetPage.tsx │ │ ├── AssetPost.tsx │ │ ├── AssetPostDrawer.tsx │ │ ├── AssetTree.tsx │ │ ├── AssetTreeChoose.tsx │ │ ├── AssetTreeModal.tsx │ │ ├── CertificateDNSProviderModal.tsx │ │ ├── CertificateIssuedLog.tsx │ │ ├── CertificateModal.tsx │ │ ├── CertificatePage.tsx │ │ ├── CredentialModal.tsx │ │ ├── CredentialPage.tsx │ │ ├── SnippetModal.tsx │ │ ├── SnippetPage.tsx │ │ ├── StorageModal.tsx │ │ ├── StoragePage.tsx │ │ ├── WebsiteAuthorised.tsx │ │ ├── WebsiteDetail.tsx │ │ ├── WebsiteInfo.tsx │ │ ├── WebsiteModal.tsx │ │ └── WebsitePage.tsx │ ├── audit │ │ ├── FileSystemLogPage.tsx │ │ ├── LoginLogPage.tsx │ │ ├── OfflineSessionPage.tsx │ │ ├── OnlineSessionPage.tsx │ │ ├── OperationLogPage.tsx │ │ ├── SessionCommandDetail.tsx │ │ ├── SessionCommandPage.tsx │ │ └── SessionCommandSummary.tsx │ ├── authorised │ │ ├── AssetAuthorised.tsx │ │ ├── AssetAuthorisedModal.tsx │ │ ├── CommandFilterDetail.tsx │ │ ├── CommandFilterInfo.tsx │ │ ├── CommandFilterModal.tsx │ │ ├── CommandFilterPage.tsx │ │ ├── CommandFilterRuleModal.tsx │ │ ├── CommandFilterRulePage.tsx │ │ ├── StrategyModal.tsx │ │ ├── StrategyPage.tsx │ │ ├── UserAuthorised.tsx │ │ └── UserAuthorisedModal.tsx │ ├── dashboard │ │ └── DashboardPage.tsx │ ├── facade │ │ ├── FacadePage.css │ │ ├── FacadePage.tsx │ │ ├── SnippetUserModal.tsx │ │ ├── SnippetUserPage.tsx │ │ └── UserInfoPage.tsx │ ├── gateway │ │ ├── AgentGatewayModal.tsx │ │ ├── AgentGatewayPage.tsx │ │ ├── AgentGatewayRegister.tsx │ │ ├── AgentGatewayStat.tsx │ │ ├── AgentGatewayTokenDrawer.tsx │ │ ├── SshGatewayModal.tsx │ │ └── SshGatewayPage.tsx │ ├── identity │ │ ├── GroupDetail.tsx │ │ ├── GroupInfo.tsx │ │ ├── GroupModal.tsx │ │ ├── GroupPage.tsx │ │ ├── LoginLockedPage.tsx │ │ ├── LoginPolicyDetailPage.tsx │ │ ├── LoginPolicyInfo.tsx │ │ ├── LoginPolicyPage.tsx │ │ ├── LoginPolicyPostPage.tsx │ │ ├── LoginPolicyUser.tsx │ │ ├── RoleDetail.tsx │ │ ├── RoleInfo.tsx │ │ ├── RoleModal.tsx │ │ ├── RolePage.tsx │ │ ├── SetupPage.tsx │ │ ├── UserDetailPage.tsx │ │ ├── UserInfo.tsx │ │ ├── UserLoginPolicy.tsx │ │ ├── UserModal.tsx │ │ ├── UserPage.tsx │ │ └── UserResetPasswordModal.tsx │ ├── sysconf │ │ ├── About.tsx │ │ ├── BackupSetting.tsx │ │ ├── IdentitySetting.tsx │ │ ├── LdapSetting.tsx │ │ ├── LicenseSetting.tsx │ │ ├── LogSetting.tsx │ │ ├── LogoSetting.tsx │ │ ├── MailSetting.tsx │ │ ├── RdpSetting.tsx │ │ ├── ReverseProxySetting.tsx │ │ ├── SecuritySetting.tsx │ │ ├── SettingPage.tsx │ │ ├── SshdSetting.tsx │ │ ├── SystemSetting.tsx │ │ ├── VncSetting.tsx │ │ └── WebAuthnSetting.tsx │ └── sysops │ │ ├── ScheduledTaskLogPage.tsx │ │ ├── ScheduledTaskModal.tsx │ │ ├── ScheduledTaskPage.tsx │ │ ├── ScheduledTaskRuntime.tsx │ │ ├── SystemMonitorPage.tsx │ │ ├── ToolsPage.tsx │ │ ├── ToolsPing.tsx │ │ ├── ToolsTcping.tsx │ │ └── ToolsTraceRoute.tsx ├── react-i18next │ ├── i18n.ts │ └── locales │ │ ├── code2json.ts │ │ ├── config.json │ │ ├── en-US.json │ │ ├── ja-JP.json │ │ ├── resources.ts │ │ ├── translates.sh │ │ ├── zh-CN.json │ │ ├── zh-CN.ts │ │ └── zh-TW.json ├── utils │ ├── array.ts │ ├── codec.ts │ ├── debounce.ts │ ├── duration.ts │ ├── global.ts │ ├── maybe.ts │ ├── permission.ts │ ├── strings.ts │ └── utils.tsx └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | ### Example user template template 2 | ### Example user template 3 | 4 | # IntelliJ project files 5 | .idea 6 | *.iml 7 | out 8 | gen 9 | node_modules 10 | dist -------------------------------------------------------------------------------- /README-zh_CN.md: -------------------------------------------------------------------------------- 1 | # Next Terminal 2 | 3 | [English](./README.md) | 简体中文 4 | 5 | 6 | 7 | ## 简介 8 | 9 | Next Terminal 是一个简洁、安全、易用的运维审计系统,支持多种远程访问协议,包括 RDP、SSH、VNC、Telnet、HTTP 等,适用于企业级运维场景。它可以记录和回放会话,协助安全审计与合规追踪。 10 | 11 | ## 快速安装 12 | 13 | [👉 安装文档](https://docs.next-terminal.typesafe.cn) 14 | 15 | ## 屏幕截图 16 | 17 | ![](screenshots/zh/dashboard.png) 18 | ![](screenshots/zh/access.png) 19 | 20 | ## 协议与条款 21 | 22 | 如您需要在企业网络中使用 next-terminal,建议先征求 IT 管理员的同意。下载、使用或分发 next-terminal 前,您必须同意 [协议](./LICENSE) 条款与限制。本项目不提供任何担保,亦不承担任何责任。 23 | 24 | 25 | ## 安全问题 26 | 27 | 如果您在使用过程中发现安全漏洞或潜在风险,请通过以下方式联系我们: 28 | 29 | 📧dushixiang@typesafe.cn 30 | 31 | ## 社群 32 | - QQ 群:938145268 33 | 34 | ## 优秀项目推荐 35 | 36 | - [go-ldap-admin:基于Go+Vue实现的openLDAP后台管理项目](https://github.com/eryajf/go-ldap-admin) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next Terminal 2 | 3 | English | [简体中文](./README-zh_CN.md) 4 | 5 | 6 | ## Introduction 7 | 8 | Next Terminal is a simple, secure, and user-friendly interactive auditing system that supports multiple remote access protocols including RDP, SSH, VNC, Telnet, and HTTP. It is designed for enterprise IT environments and helps facilitate session recording, audit tracking, and compliance reporting. 9 | 10 | ### Quick Start 11 | 12 | Refer to the installation guide here: 13 | 👉 [Installation Documentation](https://docs.next-terminal.typesafe.cn) 14 | 15 | 16 | ## Screenshots 17 | 18 | ![](screenshots/en/dashboard.png) 19 | ![](screenshots/en/access.png) 20 | 21 | ### License & Terms 22 | 23 | Before downloading, using, or distributing Next Terminal, please read and agree to the [LICENSE](./LICENSE). 24 | This project is provided "as is" without any warranties or guarantees. Use at your own risk. 25 | 26 | ⚠️ It is recommended to consult your **IT administrator** before deploying Next Terminal within a corporate network. 27 | 28 | ### Security Issues 29 | 30 | If you discover any security vulnerabilities, please contact the maintainer: 31 | 32 | 📧 [dushixiang@typesafe.cn](mailto:dushixiang@typesafe.cn) 33 | 34 | ### Recommended Projects 35 | 36 | * [go-ldap-admin](https://github.com/eryajf/go-ldap-admin): A web-based OpenLDAP management tool built with Go and Vue -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useEffect, useState } from "react" 2 | 3 | type Theme = "dark" | "light" | "system" 4 | 5 | type ThemeProviderProps = { 6 | children: React.ReactNode 7 | defaultTheme?: Theme 8 | storageKey?: string 9 | } 10 | 11 | type ThemeProviderState = { 12 | theme: Theme 13 | setTheme: (theme: Theme) => void 14 | } 15 | 16 | const initialState: ThemeProviderState = { 17 | theme: "system", 18 | setTheme: () => null, 19 | } 20 | 21 | const ThemeProviderContext = createContext(initialState) 22 | 23 | export function ThemeProvider({ 24 | children, 25 | defaultTheme = "system", 26 | storageKey = "vite-ui-theme", 27 | ...props 28 | }: ThemeProviderProps) { 29 | const [theme, setTheme] = useState( 30 | () => (localStorage.getItem(storageKey) as Theme) || defaultTheme 31 | ) 32 | 33 | useEffect(() => { 34 | const root = window.document.documentElement 35 | 36 | root.classList.remove("light", "dark") 37 | 38 | if (theme === "system") { 39 | const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") 40 | .matches 41 | ? "dark" 42 | : "light" 43 | 44 | root.classList.add(systemTheme) 45 | return 46 | } 47 | 48 | root.classList.add(theme) 49 | }, [theme]) 50 | 51 | const value = { 52 | theme, 53 | setTheme: (theme: Theme) => { 54 | localStorage.setItem(storageKey, theme) 55 | setTheme(theme) 56 | }, 57 | } 58 | 59 | return ( 60 | 61 | {children} 62 | 63 | ) 64 | } 65 | 66 | export const useTheme = () => { 67 | const context = useContext(ThemeProviderContext) 68 | 69 | if (context === undefined) 70 | throw new Error("useTheme must be used within a ThemeProvider") 71 | 72 | return context 73 | } 74 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /components/ui/input-otp.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { OTPInput, OTPInputContext } from "input-otp" 3 | import { Dot } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const InputOTP = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, containerClassName, ...props }, ref) => ( 11 | 20 | )) 21 | InputOTP.displayName = "InputOTP" 22 | 23 | const InputOTPGroup = React.forwardRef< 24 | React.ElementRef<"div">, 25 | React.ComponentPropsWithoutRef<"div"> 26 | >(({ className, ...props }, ref) => ( 27 |
28 | )) 29 | InputOTPGroup.displayName = "InputOTPGroup" 30 | 31 | const InputOTPSlot = React.forwardRef< 32 | React.ElementRef<"div">, 33 | React.ComponentPropsWithoutRef<"div"> & { index: number } 34 | >(({ index, className, ...props }, ref) => { 35 | const inputOTPContext = React.useContext(OTPInputContext) 36 | const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index] 37 | 38 | return ( 39 |
48 | {char} 49 | {hasFakeCaret && ( 50 |
51 |
52 |
53 | )} 54 |
55 | ) 56 | }) 57 | InputOTPSlot.displayName = "InputOTPSlot" 58 | 59 | const InputOTPSeparator = React.forwardRef< 60 | React.ElementRef<"div">, 61 | React.ComponentPropsWithoutRef<"div"> 62 | >(({ ...props }, ref) => ( 63 |
64 | 65 |
66 | )) 67 | InputOTPSeparator.displayName = "InputOTPSeparator" 68 | 69 | export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } 70 | -------------------------------------------------------------------------------- /components/ui/resizable.tsx: -------------------------------------------------------------------------------- 1 | import { GripVertical } from "lucide-react" 2 | import * as ResizablePrimitive from "react-resizable-panels" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const ResizablePanelGroup = ({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) => ( 10 | 17 | ) 18 | 19 | const ResizablePanel = ResizablePrimitive.Panel 20 | 21 | const ResizableHandle = ({ 22 | withHandle, 23 | className, 24 | ...props 25 | }: React.ComponentProps & { 26 | withHandle?: boolean 27 | }) => ( 28 | div]:rotate-90", 31 | className 32 | )} 33 | {...props} 34 | > 35 | {withHandle && ( 36 |
37 | 38 |
39 | )} 40 |
41 | ) 42 | 43 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle } 44 | -------------------------------------------------------------------------------- /components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const ScrollArea = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, children, ...props }, ref) => ( 10 | 15 | 16 | {children} 17 | 18 | 19 | 20 | 21 | )) 22 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 23 | 24 | const ScrollBar = React.forwardRef< 25 | React.ElementRef, 26 | React.ComponentPropsWithoutRef 27 | >(({ className, orientation = "vertical", ...props }, ref) => ( 28 | 41 | 42 | 43 | )) 44 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 45 | 46 | export { ScrollArea, ScrollBar } 47 | -------------------------------------------------------------------------------- /components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const Separator = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >( 10 | ( 11 | { className, orientation = "horizontal", decorative = true, ...props }, 12 | ref 13 | ) => ( 14 | 25 | ) 26 | ) 27 | Separator.displayName = SeparatorPrimitive.Root.displayName 28 | 29 | export { Separator } 30 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {title} 7 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-terminal-web", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@ant-design/pro-components": "^2.8.7", 13 | "@dushixiang/guacamole-common-js": "1.5.5-fixed", 14 | "@monaco-editor/react": "^4.6.0", 15 | "@radix-ui/react-scroll-area": "^1.2.0", 16 | "@radix-ui/react-separator": "^1.1.0", 17 | "@radix-ui/react-slot": "^1.2.3", 18 | "@simplewebauthn/browser": "^13.0.0", 19 | "@tanstack/react-query": "^5.7.2", 20 | "@types/qs": "^6.9.9", 21 | "@xterm/addon-fit": "^0.10.0", 22 | "@xterm/xterm": "^5.5.0", 23 | "antd": "^5.25.1", 24 | "asciinema-player": "3.8.0", 25 | "class-variance-authority": "^0.7.1", 26 | "clsx": "^2.1.1", 27 | "framer-motion": "^12.12.1", 28 | "i18next": "^23.6.0", 29 | "input-otp": "^1.4.1", 30 | "jotai": "^2.9.0", 31 | "js-base64": "^3.7.7", 32 | "lucide-react": "^0.509.0", 33 | "qs": "^6.11.2", 34 | "react": "^18.2.0", 35 | "react-countup": "^6.5.3", 36 | "react-dom": "^18.2.0", 37 | "react-fast-marquee": "^1.6.5", 38 | "react-i18next": "^14.0.1", 39 | "react-resizable-panels": "^2.0.22", 40 | "react-router-dom": "^6.18.0", 41 | "react-spinners": "^0.14.1", 42 | "react-use": "^17.4.0", 43 | "react-window": "^1.8.11", 44 | "recharts": "^2.12.7", 45 | "simplebar-react": "^3.2.6", 46 | "tailwind-merge": "^2.3.0", 47 | "tailwindcss-animate": "^1.0.7" 48 | }, 49 | "devDependencies": { 50 | "@types/node": "^20.4.0", 51 | "@types/react": "^18.2.50", 52 | "@types/react-dom": "^18.0.8", 53 | "@types/react-window": "^1.8.8", 54 | "@vitejs/plugin-react": "^4.2.1", 55 | "autoprefixer": "^10.4.16", 56 | "postcss": "^8.4.31", 57 | "tailwindcss": "^3.3.5", 58 | "typescript": "^5.3.3", 59 | "vite": "6.3.4" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/public/logo.png -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screenshots/en/access.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/screenshots/en/access.png -------------------------------------------------------------------------------- /screenshots/en/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/screenshots/en/dashboard.png -------------------------------------------------------------------------------- /screenshots/zh/access.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/screenshots/zh/access.png -------------------------------------------------------------------------------- /screenshots/zh/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/screenshots/zh/dashboard.png -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .ant-menu-light.ant-menu-root.ant-menu-inline, 2 | .ant-menu-light.ant-menu-root.ant-menu-vertical { 3 | border-inline-end: unset !important; 4 | } 5 | 6 | .x-side { 7 | box-shadow: 1px 0 14px rgba(0, 21, 41, .08); 8 | border-inline-end: 1px solid rgba(255, 255, 255, 0.06) 9 | } 10 | 11 | .page-herder { 12 | margin: 16px 16px 0 16px; 13 | } 14 | 15 | .page-search { 16 | background-color: white; 17 | } 18 | 19 | .page-search label { 20 | font-weight: bold; 21 | } 22 | 23 | .page-search .ant-form-item { 24 | margin-bottom: 0; 25 | } 26 | 27 | .page-card { 28 | margin: 16px; 29 | } 30 | 31 | .modal-no-padding .ant-modal-body { 32 | padding: 0; 33 | } 34 | 35 | .modal-no-padding-bg-xterm .ant-modal-body { 36 | background-color: #121314; 37 | } 38 | 39 | .disabled-icon { 40 | cursor: not-allowed; 41 | color: #ccc; 42 | } 43 | 44 | .disabled-icon:hover { 45 | color: #ccc; 46 | } 47 | 48 | .ant-page-header { 49 | padding: 0 !important; 50 | } 51 | 52 | .danger { 53 | color: red; 54 | } 55 | 56 | .danger:hover { 57 | color: red !important; 58 | } 59 | 60 | .app-page-container { 61 | background-color: white; 62 | padding: 16px; 63 | } 64 | 65 | .site-layout { 66 | width: 80%; 67 | margin: 20px auto; 68 | } 69 | 70 | @keyframes spin { 71 | 0% { 72 | transform: rotate(0deg); 73 | } 74 | 100% { 75 | transform: rotate(360deg); 76 | } 77 | } 78 | 79 | .loading-icon { 80 | animation: spin 1s linear infinite; 81 | } 82 | 83 | /* 整体滚动条样式 */ 84 | .xterm-viewport::-webkit-scrollbar { 85 | width: 10px; 86 | cursor: pointer; 87 | z-index: 10 !important; 88 | } 89 | 90 | /* 滚动条滑块样式 */ 91 | .xterm-viewport::-webkit-scrollbar-thumb { 92 | background-color: #181818 !important; 93 | border-radius: 8px; 94 | background-clip: content-box; 95 | border: 2px solid transparent; 96 | cursor: pointer; 97 | z-index: 10 !important; 98 | } 99 | 100 | .xterm-viewport[scroll]::-webkit-scrollbar-thumb, 101 | .xterm-viewport::-webkit-scrollbar-thumb:hover { 102 | background-color: #141414 !important; 103 | } 104 | 105 | .ant-table-container .ant-table-body, 106 | .ant-table-container .ant-table-content { 107 | scrollbar-width: thin; 108 | scrollbar-color: #eaeaea transparent; 109 | scrollbar-gutter: stable; 110 | } -------------------------------------------------------------------------------- /src/api/access-setting-api.ts: -------------------------------------------------------------------------------- 1 | import requests from "@/src/api/core/requests"; 2 | import strings from "@/src/utils/strings"; 3 | 4 | export type Setting = { 5 | fontSize: number; 6 | lineHeight: number; 7 | fontFamily: string; 8 | selectionCopy: boolean; 9 | rightClickPaste: boolean; 10 | } 11 | 12 | class AccessSettingApi { 13 | get = async () => { 14 | let record = await requests.get('/access/settings') as Record; 15 | let setting: Setting = { 16 | fontSize: parseInt(record['fontSize']), 17 | lineHeight: parseFloat(record['lineHeight']), 18 | fontFamily: record['fontFamily'], 19 | selectionCopy: strings.isTrue(record['selectionCopy']), 20 | rightClickPaste: strings.isTrue(record['rightClickPaste']), 21 | } 22 | return setting; 23 | } 24 | 25 | set = async (data: Record) => { 26 | await requests.put('/access/settings', data); 27 | } 28 | } 29 | 30 | export default new AccessSettingApi(); -------------------------------------------------------------------------------- /src/api/agent-gateway-api.ts: -------------------------------------------------------------------------------- 1 | import {Api} from "./core/api"; 2 | import requests from "@/src/api/core/requests"; 3 | 4 | export interface AgentGateway { 5 | id: string; 6 | name: string; 7 | ip: string; 8 | os: string; 9 | arch: string; 10 | online: boolean; 11 | createdAt: number; 12 | updatedAt: number; 13 | stat?: Stat; 14 | } 15 | 16 | interface Stat { 17 | cpu: CPU; 18 | memory: Memory; 19 | disk: Disk; 20 | disk_io: DiskIO; 21 | network: Network; 22 | load: Load; 23 | host: Host; 24 | process: Process; 25 | connections: any; 26 | tcp_states: any[]; 27 | security_alerts?: any; 28 | errors: Errors; 29 | } 30 | 31 | interface Errors { 32 | } 33 | 34 | interface Process { 35 | total: number; 36 | } 37 | 38 | interface Host { 39 | hostname: string; 40 | os: string; 41 | arch: string; 42 | version: string; 43 | uptime: number; 44 | } 45 | 46 | interface Load { 47 | load_1: number; 48 | load_5: number; 49 | load_15: number; 50 | } 51 | 52 | interface Network { 53 | rx: number; 54 | tx: number; 55 | rx_sec: number; 56 | tx_sec: number; 57 | history: any[]; 58 | } 59 | 60 | interface DiskIO { 61 | read_bytes: number; 62 | write_bytes: number; 63 | history: any[]; 64 | } 65 | 66 | interface Disk { 67 | total: number; 68 | used: number; 69 | percent: number; 70 | history: any[]; 71 | } 72 | 73 | interface Memory { 74 | total: number; 75 | used: number; 76 | free: number; 77 | percent: number; 78 | swap_total: number; 79 | swap_free: number; 80 | history: any[]; 81 | } 82 | 83 | interface CPU { 84 | model: string; 85 | physical_cores: number; 86 | logical_cores: number; 87 | percent: number; 88 | history: any[]; 89 | } 90 | 91 | export interface RegisterParam { 92 | endpoint: string; 93 | token: string; 94 | } 95 | 96 | class AgentGatewayApi extends Api { 97 | constructor() { 98 | super("admin/agent-gateways"); 99 | } 100 | 101 | getRegisterParam = async () => { 102 | return await requests.get(`/${this.group}/get-register-param`) as RegisterParam; 103 | } 104 | 105 | setRegisterAddr = async (endpoint: string) => { 106 | return await requests.post(`/${this.group}/set-register-addr?endpoint=${endpoint}`); 107 | } 108 | 109 | getStat = async (id: string) => { 110 | return await requests.get(`/${this.group}/${id}/stat`) as Stat; 111 | } 112 | } 113 | 114 | let agentGatewayApi = new AgentGatewayApi(); 115 | export default agentGatewayApi; -------------------------------------------------------------------------------- /src/api/agent-gateway-token-api.ts: -------------------------------------------------------------------------------- 1 | import {Api} from "./core/api"; 2 | 3 | export interface AgentGatewayToken { 4 | id: string; 5 | remark: string; 6 | updatedAt: number; 7 | } 8 | 9 | class AgentGatewayTokenApi extends Api { 10 | constructor() { 11 | super("admin/agent-gateway-tokens"); 12 | } 13 | } 14 | 15 | let agentGatewayTokenApi = new AgentGatewayTokenApi(); 16 | export default agentGatewayTokenApi; -------------------------------------------------------------------------------- /src/api/anonymous-api.ts: -------------------------------------------------------------------------------- 1 | import requests from "@/src/api/core/requests"; 2 | import {Strategy} from "@/src/api/strategy-api"; 3 | import {SessionWatermark} from "@/src/api/session-api"; 4 | 5 | export interface AnonymousSession { 6 | id: string 7 | assetName: string 8 | strategy: Strategy 9 | watermark: SessionWatermark 10 | } 11 | 12 | class AnonymousApi { 13 | getSessionById = async (sessionId: string) => { 14 | return await requests.get(`/access/session?sessionId=${sessionId}`) as AnonymousSession; 15 | } 16 | } 17 | 18 | let anonymousApi = new AnonymousApi(); 19 | export default anonymousApi; -------------------------------------------------------------------------------- /src/api/authorised-asset-api.ts: -------------------------------------------------------------------------------- 1 | import qs from "qs"; 2 | import requests from "./core/requests"; 3 | 4 | export interface Authorised { 5 | id: string; 6 | assetId: string; 7 | assetName: string; 8 | commandFilterId: string; 9 | commandFilterName: string; 10 | strategyId: string; 11 | strategyName: string; 12 | userId: string; 13 | userName: string; 14 | userGroupId: string; 15 | userGroupName: string; 16 | expiredAt: number; 17 | createdAt: number; 18 | assetGroupName: string; 19 | } 20 | 21 | class AuthorisedAssetApi { 22 | 23 | group = "admin/authorised-asset"; 24 | 25 | paging = async (params: any) => { 26 | let paramsStr = qs.stringify(params); 27 | return await requests.get(`/${this.group}/paging?${paramsStr}`); 28 | } 29 | 30 | authorisedAssets = async (data: any) => { 31 | return await requests.post(`/${this.group}/assets`, data); 32 | } 33 | 34 | authorisedUsers = async (data: any) => { 35 | return await requests.post(`/${this.group}/users`, data); 36 | } 37 | 38 | authorisedUserGroups = async (data: any) => { 39 | return await requests.post(`/${this.group}/user-groups`, data); 40 | } 41 | 42 | selected = async (expect: string, userId?: string, userGroupId?: string, assetId?: string) => { 43 | let paramsStr = qs.stringify({expect, userId, userGroupId, assetId}); 44 | return await requests.get(`/${this.group}/selected?${paramsStr}`) as String[]; 45 | } 46 | 47 | deleteById = async (id: string) => { 48 | await requests.delete(`/${this.group}/${id}`) 49 | } 50 | 51 | getById = async (id: string) => { 52 | return await requests.get(`/${this.group}/${id}`) 53 | } 54 | 55 | update = async (id: string, values: any) => { 56 | await requests.put(`/${this.group}/${id}`, values) 57 | } 58 | } 59 | 60 | const authorisedAssetApi = new AuthorisedAssetApi(); 61 | export default authorisedAssetApi; -------------------------------------------------------------------------------- /src/api/authorised-website-api.ts: -------------------------------------------------------------------------------- 1 | import requests from "./core/requests"; 2 | 3 | class AuthorisedWebsiteApi { 4 | 5 | group = "admin/authorised-website"; 6 | 7 | bindingUser = async (websiteId: string, userIds: string[]) => { 8 | return await requests.post(`/${this.group}/binding-user?websiteId=${websiteId}`, userIds); 9 | } 10 | 11 | unboundUser = async (websiteId: string, userIds: string[]) => { 12 | return await requests.post(`/${this.group}/unbound-user?websiteId=${websiteId}`, userIds); 13 | } 14 | 15 | boundUser = async (websiteId: string) => { 16 | return await requests.get(`/${this.group}/bound-user?websiteId=${websiteId}`) as string[]; 17 | } 18 | 19 | bindingUserGroup = async (websiteId: string, userIds: string[]) => { 20 | return await requests.post(`/${this.group}/binding-user-group?websiteId=${websiteId}`, userIds); 21 | } 22 | 23 | unboundUserGroup = async (websiteId: string, userIds: string[]) => { 24 | return await requests.post(`/${this.group}/unbound-user-group?websiteId=${websiteId}`, userIds); 25 | } 26 | 27 | boundUserGroup = async (websiteId: string) => { 28 | return await requests.get(`/${this.group}/bound-user-group?websiteId=${websiteId}`) as string[]; 29 | } 30 | } 31 | 32 | const authorisedWebsiteApi = new AuthorisedWebsiteApi(); 33 | export default authorisedWebsiteApi; -------------------------------------------------------------------------------- /src/api/branding-api.ts: -------------------------------------------------------------------------------- 1 | import requests, {baseUrl} from "./core/requests"; 2 | import {global} from "@/src/utils/global"; 3 | import strings from "@/src/utils/strings"; 4 | 5 | export interface Branding { 6 | copyright: string; 7 | logo: string; 8 | name: string; 9 | root: string; 10 | version: string; 11 | dev: boolean 12 | loginBackgroundColor: string 13 | icp: string 14 | hiddenUpgrade: boolean 15 | } 16 | 17 | class BrandingApi { 18 | getBranding = async () => { 19 | if (strings.hasText(global.branding?.name)) { 20 | return new Promise((resolve, reject) => { 21 | resolve(global.branding); 22 | }); 23 | } 24 | return await requests.get(`/branding`) as Branding; 25 | } 26 | 27 | getLogo = () => { 28 | return `${baseUrl()}/logo`; 29 | } 30 | } 31 | 32 | let brandingApi = new BrandingApi(); 33 | export default brandingApi; -------------------------------------------------------------------------------- /src/api/certificate-api.ts: -------------------------------------------------------------------------------- 1 | import {Api} from "./core/api"; 2 | import requests from "./core/requests"; 3 | 4 | export interface Certificate { 5 | id: string; 6 | commonName: string; 7 | subject: string; 8 | issuer: string; 9 | notBefore: number; 10 | notAfter: number; 11 | type: string; 12 | storageKey: string; 13 | certificate: string; 14 | privateKey: string; 15 | issuedStatus: string; 16 | issuedError: string; 17 | updatedAt: number; 18 | isDefault: boolean; 19 | } 20 | 21 | class CertificateApi extends Api { 22 | constructor() { 23 | super("admin/certificates"); 24 | } 25 | 26 | updateAsDefault = async (id: string) => { 27 | await requests.patch(`/${this.group}/${id}/default`) 28 | } 29 | } 30 | 31 | let certificateApi = new CertificateApi(); 32 | export default certificateApi; -------------------------------------------------------------------------------- /src/api/command-filter-api.ts: -------------------------------------------------------------------------------- 1 | import {Api} from "@/src/api/core/api"; 2 | import requests from "@/src/api/core/requests"; 3 | 4 | export interface CommandFilter { 5 | id: string; 6 | name: string; 7 | createdAt: number; 8 | } 9 | 10 | class CommandFilterApi extends Api{ 11 | 12 | constructor() { 13 | super("admin/command-filters"); 14 | } 15 | 16 | Bind = async (id: string, data: any) => { 17 | await requests.post(`/${this.group}/${id}/bind`, data) 18 | } 19 | 20 | Unbind = async (id: string, data: any) => { 21 | await requests.post(`/${this.group}/${id}/unbind`, data); 22 | } 23 | } 24 | 25 | const commandFilterApi = new CommandFilterApi(); 26 | export default commandFilterApi; -------------------------------------------------------------------------------- /src/api/command-filter-rule-api.ts: -------------------------------------------------------------------------------- 1 | import {Api} from "@/src/api/core/api"; 2 | 3 | export interface CommandFilterRule { 4 | id: string; 5 | commandFilterId: string; 6 | type: string; 7 | command: string; 8 | priority: number; 9 | enabled: boolean; 10 | action: string; 11 | } 12 | 13 | class CommandFilterRuleApi extends Api{ 14 | constructor() { 15 | super("admin/command-filter-rules"); 16 | } 17 | } 18 | 19 | const commandFilterRuleApi = new CommandFilterRuleApi(); 20 | export default commandFilterRuleApi; -------------------------------------------------------------------------------- /src/api/core/api.ts: -------------------------------------------------------------------------------- 1 | import requests from "./requests"; 2 | import qs from "qs"; 3 | 4 | export class PageParam extends Map { 5 | public pageIndex: number | undefined; 6 | public pageSize: number | undefined; 7 | 8 | constructor(pageIndex: number, pageSize: number) { 9 | super(); 10 | this.pageIndex = pageIndex; 11 | this.pageSize = pageSize 12 | } 13 | } 14 | 15 | export type PageData = { 16 | items: T[], 17 | total: number, 18 | } 19 | 20 | export class Api { 21 | group = ""; 22 | 23 | constructor(group: string) { 24 | this.group = group; 25 | } 26 | 27 | getById = async (id: string) => { 28 | let result = await requests.get(`/${this.group}/${id}`); 29 | return result as T; 30 | } 31 | 32 | getPaging = async (params: {}) => { 33 | let paramsStr = qs.stringify(params); 34 | let result = await requests.get(`/${this.group}/paging?${paramsStr}`); 35 | return result as PageData; 36 | } 37 | 38 | getAll = async () => { 39 | let result = await requests.get(`/${this.group}`); 40 | return result as T[]; 41 | } 42 | 43 | create = async (data: T) => { 44 | const result = await requests.post(`/${this.group}`, data); 45 | return result as T; 46 | } 47 | 48 | updateById = async (id: string, data: T) => { 49 | await requests.put(`/${this.group}/${id}`, data); 50 | } 51 | 52 | deleteById = async (id: string) => { 53 | await requests.delete(`/${this.group}/${id}`); 54 | } 55 | } -------------------------------------------------------------------------------- /src/api/core/event-emitter.ts: -------------------------------------------------------------------------------- 1 | const eventNames = [ 2 | "NETWORK:UN_CONNECT", 3 | "UI:LOADING", 4 | "API:UN_AUTH", "API:VALIDATE_ERROR", "API:NEED_ENABLE_OPT", "API:NEED_CHANGE_PASSWORD", 5 | "WS:MESSAGE", 6 | ]; 7 | type EventNames = (typeof eventNames)[number]; 8 | 9 | class EventEmitter { 10 | private listeners: Record> = {}; 11 | 12 | on(eventName: EventNames, listener: Function) { 13 | if (!this.listeners[eventName]) { 14 | this.listeners[eventName] = new Set(); 15 | } 16 | this.listeners[eventName].add(listener); 17 | } 18 | 19 | off(eventName: EventNames, listener: Function) { 20 | if (!this.listeners[eventName]) { 21 | return; 22 | } 23 | this.listeners[eventName].delete(listener); 24 | } 25 | 26 | emit(eventName: EventNames, ...args: any[]) { 27 | if (!this.listeners[eventName]) { 28 | return; 29 | } 30 | this.listeners[eventName].forEach(listener => { 31 | listener(...args); 32 | }); 33 | } 34 | } 35 | 36 | export default new EventEmitter(); -------------------------------------------------------------------------------- /src/api/credential-api.ts: -------------------------------------------------------------------------------- 1 | import {Api} from "./core/api"; 2 | import requests from "@/src/api/core/requests"; 3 | 4 | export interface Credential { 5 | id: string; 6 | name: string; 7 | type: string; 8 | username: string; 9 | password: string; 10 | privateKey: string; 11 | passphrase: string; 12 | createdAt: number; 13 | } 14 | 15 | class CredentialApi extends Api { 16 | constructor() { 17 | super("admin/credentials"); 18 | } 19 | 20 | genPrivateKey = async () => { 21 | return await requests.post(`/${this.group}/gen-private-key`) as string; 22 | } 23 | 24 | getPublicKey = async (id: string) => { 25 | return await requests.get(`/${this.group}/${id}/public-key`) as string; 26 | } 27 | 28 | decrypt = async (id: string, securityToken: string) => { 29 | return await requests.get(`/${this.group}/${id}/decrypted?securityToken=${securityToken}`) as Credential; 30 | } 31 | } 32 | 33 | let credentialApi = new CredentialApi(); 34 | export default credentialApi; -------------------------------------------------------------------------------- /src/api/dashboard-api.ts: -------------------------------------------------------------------------------- 1 | import requests from "@/src/api/core/requests"; 2 | 3 | export interface TimeCounter { 4 | loginFailedTimes: number; 5 | sessionOnlineCount: number; 6 | sessionTotalCount: number; 7 | userOnlineCount: number; 8 | userTotalCount: number; 9 | assetActiveCount: number; 10 | assetTotalCount: number; 11 | websiteActiveCount: number; 12 | websiteTotalCount: number; 13 | gatewayActiveCount: number; 14 | gatewayTotalCount: number; 15 | } 16 | 17 | export interface TypeValue { 18 | type: string; 19 | value: number; 20 | } 21 | 22 | class DashboardApi { 23 | 24 | getTimeCounter = async () => { 25 | return await requests.get(`/admin/dashboard/time-counter`) as TimeCounter; 26 | } 27 | 28 | getDateCounter = async () => { 29 | return await requests.get(`/admin/dashboard/date-counter`); 30 | } 31 | 32 | getDateCounterV2 = async () => { 33 | return await requests.get(`/admin/dashboard/v2/date-counter`); 34 | } 35 | 36 | getAssetTypes = async () => { 37 | return await requests.get(`/admin/dashboard/asset-types`) as TypeValue[]; 38 | } 39 | } 40 | 41 | let dashboardApi = new DashboardApi(); 42 | export default dashboardApi; -------------------------------------------------------------------------------- /src/api/dns-provider-api.ts: -------------------------------------------------------------------------------- 1 | import requests from "@/src/api/core/requests"; 2 | 3 | export interface DNSProvider { 4 | ok: boolean; 5 | email: string; 6 | type: string; 7 | tencentcloud: Tencentcloud; 8 | alidns: Alidns; 9 | cloudflare: Cloudflare; 10 | huaweicloud: Huaweicloud; 11 | } 12 | 13 | interface Huaweicloud { 14 | accessKeyId: string; 15 | secretAccessKey: string; 16 | } 17 | 18 | interface Cloudflare { 19 | apiToken: string; 20 | zoneToken: string; 21 | } 22 | 23 | interface Alidns { 24 | accessKeyId: string; 25 | accessKeySecret: string; 26 | } 27 | 28 | interface Tencentcloud { 29 | secretId: string; 30 | secretKey: string; 31 | } 32 | 33 | class DnsProviderApi { 34 | group = "admin/dns-providers"; 35 | 36 | get = async () => { 37 | return await requests.get(`/${this.group}/config`) as DNSProvider; 38 | } 39 | 40 | set = async (values: any) => { 41 | return await requests.put(`/${this.group}/config`, values); 42 | } 43 | 44 | remove = async () => { 45 | await requests.delete(`/${this.group}/config`); 46 | } 47 | } 48 | 49 | let dnsProviderApi = new DnsProviderApi(); 50 | export default dnsProviderApi; -------------------------------------------------------------------------------- /src/api/filesystem-api.ts: -------------------------------------------------------------------------------- 1 | import requests from "@/src/api/core/requests"; 2 | 3 | export interface FileInfo { 4 | name: string 5 | size: number 6 | modTime: string 7 | path: string 8 | mode: string 9 | isDir: boolean 10 | isLink: boolean 11 | } 12 | 13 | export interface Progress { 14 | total: number 15 | written: number 16 | percent: number 17 | speed: number 18 | } 19 | 20 | class FileSystemApi { 21 | group = "access/filesystem"; 22 | 23 | ls = async (sessionId: string, dir: string, hiddenFileVisible: boolean) => { 24 | return await requests.get(`/${this.group}/${sessionId}/ls?dir=${dir}&hiddenFileVisible=${hiddenFileVisible}`) as FileInfo[]; 25 | } 26 | 27 | rm = async (sessionId: string, filename: string) => { 28 | await requests.post(`/${this.group}/${sessionId}/rm?filename=${filename}`); 29 | } 30 | 31 | mkdir = async (sessionId: string, dir: string) => { 32 | await requests.post(`/${this.group}/${sessionId}/mkdir?dir=${dir}`); 33 | } 34 | 35 | touch = async (sessionId: string, filename: string) => { 36 | await requests.post(`/${this.group}/${sessionId}/touch?filename=${filename}`); 37 | } 38 | 39 | rename = async (sessionId: string, oldName: string, newName: string) => { 40 | await requests.post(`/${this.group}/${sessionId}/rename?oldName=${oldName}&newName=${newName}`); 41 | } 42 | 43 | edit = async (sessionId: string, filename: string, fileContent: string) => { 44 | await requests.post(`/${this.group}/${sessionId}/edit`, { 45 | filename, 46 | fileContent 47 | }); 48 | } 49 | 50 | uploadProgress = async (id: string) => { 51 | let data = await requests.get(`/${this.group}/upload/progress?id=${id}`); 52 | return data as Progress; 53 | } 54 | } 55 | 56 | const fileSystemApi = new FileSystemApi(); 57 | export default fileSystemApi; 58 | -------------------------------------------------------------------------------- /src/api/fileystem-log-api.ts: -------------------------------------------------------------------------------- 1 | import {Api} from "@/src/api/core/api"; 2 | import requests from "@/src/api/core/requests"; 3 | 4 | export interface FileSystemLog { 5 | id: string; 6 | assetId: string; 7 | sessionId: string; 8 | userId: string; 9 | action: string; 10 | fileName: string; 11 | createdAt: number; 12 | assetName: string; 13 | userName: string; 14 | } 15 | 16 | class FileSystemLogApi extends Api { 17 | constructor() { 18 | super("admin/filesystem-logs"); 19 | } 20 | 21 | clear = async () => { 22 | await requests.post(`/${this.group}/clear`); 23 | } 24 | } 25 | 26 | const fileSystemLogApi = new FileSystemLogApi(); 27 | export default fileSystemLogApi; -------------------------------------------------------------------------------- /src/api/license-api.ts: -------------------------------------------------------------------------------- 1 | import requests from "./core/requests"; 2 | 3 | export interface License { 4 | type: string; 5 | machineId: string; 6 | asset: number; 7 | concurrent: number; 8 | user: number; 9 | expired: number; 10 | } 11 | 12 | // 定义 License 类 13 | export class SimpleLicense { 14 | type: string | '' | 'free' | 'test' | 'premium' | 'enterprise'; 15 | expired?: number; 16 | oem?: boolean; 17 | 18 | constructor(type: string, expired?: number, oem?: boolean) { 19 | this.type = type; 20 | this.expired = expired; 21 | this.oem = oem; 22 | } 23 | 24 | // 添加方法 25 | isPremium(): boolean { 26 | return this.type === 'premium'; 27 | } 28 | 29 | isEnterprise(): boolean { 30 | return this.type === 'enterprise'; 31 | } 32 | 33 | isTest(): boolean { 34 | return this.type === 'test'; 35 | } 36 | 37 | isFree(): boolean { 38 | return this.type === '' || this.type === 'free'; 39 | } 40 | 41 | isExpired(): boolean { 42 | return this.expired !== undefined && this.expired > 0 && this.expired < new Date().getTime(); 43 | } 44 | 45 | isOEM(): boolean { 46 | console.log("isOEM", this.oem) 47 | return this.oem === true; 48 | } 49 | } 50 | 51 | class LicenseApi { 52 | 53 | group = "/admin/license"; 54 | 55 | getMachineId = async () => { 56 | return await requests.get(`${this.group}/machine-id`); 57 | } 58 | 59 | getLicense = async () => { 60 | return await requests.get(`${this.group}`) as License; 61 | } 62 | 63 | getSimpleLicense = async () => { 64 | let data = await requests.get(`/license`); 65 | return new SimpleLicense(data.type, data.expired, data.oem); 66 | } 67 | 68 | setLicense = async (values: any) => { 69 | await requests.post(`${this.group}`, values); 70 | } 71 | 72 | requestLicense = async () => { 73 | await requests.post(`${this.group}/request`); 74 | } 75 | } 76 | 77 | let licenseApi = new LicenseApi(); 78 | export default licenseApi; -------------------------------------------------------------------------------- /src/api/login-locked-api.ts: -------------------------------------------------------------------------------- 1 | import {Api} from "./core/api"; 2 | 3 | export interface LoginLocked { 4 | id: string 5 | type: string 6 | } 7 | 8 | class LoginLockedApi extends Api { 9 | constructor() { 10 | super("admin/login-locked"); 11 | } 12 | } 13 | 14 | let loginLockedApi = new LoginLockedApi(); 15 | export default loginLockedApi; -------------------------------------------------------------------------------- /src/api/login-log-api.ts: -------------------------------------------------------------------------------- 1 | import {Api} from "./core/api"; 2 | import requests from "@/src/api/core/requests"; 3 | import {LoginLog} from "@/src/api/user-api"; 4 | 5 | class LoginLogApi extends Api { 6 | constructor() { 7 | super("admin/login-logs"); 8 | } 9 | 10 | clear = async () => { 11 | await requests.post(`/${this.group}/clear`); 12 | } 13 | } 14 | 15 | let loginLogApi = new LoginLogApi(); 16 | export default loginLogApi; -------------------------------------------------------------------------------- /src/api/login-policy-api.ts: -------------------------------------------------------------------------------- 1 | import {Api} from "./core/api"; 2 | import requests from "./core/requests"; 3 | 4 | export interface LoginPolicy { 5 | id: string; 6 | name: string; 7 | ipGroup: string; 8 | priority: number; 9 | enabled: boolean; 10 | rule: string; 11 | expirationAt?: number; 12 | timePeriod: TimePeriod[]; 13 | createdAt: number; 14 | } 15 | 16 | interface TimePeriod { 17 | key: number; 18 | value: string; 19 | } 20 | 21 | class LoginPolicyApi extends Api { 22 | 23 | constructor() { 24 | super("admin/login-policies"); 25 | } 26 | 27 | bindUser = async (loginPolicyId: string, data: string[]) => { 28 | await requests.post(`/${this.group}/bind-user-id?loginPolicyId=${loginPolicyId}`, data); 29 | } 30 | 31 | unbindUser = async (loginPolicyId: string, data: string[]) => { 32 | await requests.post(`/${this.group}/unbind-user-id?loginPolicyId=${loginPolicyId}`, data); 33 | } 34 | 35 | bindLoginPolicy = async (userId: string, data: string[]) => { 36 | await requests.post(`/${this.group}/bind-login-policy-id?userId=${userId}`, data); 37 | } 38 | 39 | unbindLoginPolicy = async (userId: string, data: string[]) => { 40 | await requests.post(`/${this.group}/unbind-login-policy-id?userId=${userId}`, data); 41 | } 42 | 43 | getUserId = async (loginPolicyId: string) => { 44 | return await requests.get(`/${this.group}/user-id?loginPolicyId=${loginPolicyId}`) as string[]; 45 | } 46 | 47 | getLoginPolicyIdByUserId = async (userId: string) => { 48 | return await requests.get(`/${this.group}/login-policy-id?userId=${userId}`); 49 | } 50 | } 51 | 52 | const loginPolicyApi = new LoginPolicyApi(); 53 | export default loginPolicyApi; -------------------------------------------------------------------------------- /src/api/logo-api.ts: -------------------------------------------------------------------------------- 1 | import requests from "@/src/api/core/requests"; 2 | 3 | interface LogoImage { 4 | name: string; 5 | data: string; 6 | deletable: boolean; 7 | } 8 | 9 | class LogoApi { 10 | group = `admin/logos`; 11 | 12 | logos = async () => { 13 | return await requests.get(`/${this.group}`) as LogoImage[]; 14 | } 15 | 16 | upload = async (file: File) => { 17 | const formData = new FormData(); 18 | formData.append("file", file); 19 | return await requests.postForm(`/${this.group}/upload`, formData); 20 | } 21 | 22 | delete = async (name: string) => { 23 | return await requests.delete(`/${this.group}/${name}`); 24 | } 25 | } 26 | 27 | export const logoApi = new LogoApi(); -------------------------------------------------------------------------------- /src/api/operation-log-api.ts: -------------------------------------------------------------------------------- 1 | import {Api} from "./core/api"; 2 | import requests from "@/src/api/core/requests"; 3 | import {UserAgent} from "@/src/api/user-api"; 4 | 5 | export interface OperationLog { 6 | id: string; 7 | accountId: string; 8 | accountName: string; 9 | action: string; 10 | content: string; 11 | ip: string; 12 | region: string; 13 | userAgent: UserAgent; 14 | status: string; 15 | errorMessage: string; 16 | remark: string; 17 | createdAt: number; 18 | } 19 | 20 | 21 | class OperationLogApi extends Api { 22 | constructor() { 23 | super("admin/operation-logs"); 24 | } 25 | 26 | clear = async () => { 27 | await requests.post(`/${this.group}/clear`); 28 | } 29 | } 30 | 31 | let operationLogApi = new OperationLogApi(); 32 | export default operationLogApi; -------------------------------------------------------------------------------- /src/api/property-api.ts: -------------------------------------------------------------------------------- 1 | import requests from "./core/requests"; 2 | import strings from "../utils/strings"; 3 | 4 | export interface LatestVersion { 5 | currentVersion: string; 6 | latestVersion: string; 7 | upgrade: boolean; 8 | content: string; 9 | } 10 | 11 | const booleanKeys = [ 12 | 'watermark-content-user-account', 13 | 'watermark-content-asset-username', 14 | 'reverse-proxy-server-auto-tls', 15 | 'reverse-proxy-server-http-redirect-to-https', 16 | 'login-session-count-custom', 17 | 'ssh-server-port-forwarding-enabled', 18 | 'access-require-mfa', 19 | 'ssh-server-disable-password-auth', 20 | ] 21 | 22 | export interface UpgradeStatus { 23 | message: string; 24 | status: string; 25 | } 26 | 27 | class PropertyApi { 28 | group = "admin/properties"; 29 | 30 | get = async () => { 31 | let properties = await requests.get(`/${this.group}`); 32 | for (let key in properties) { 33 | if (!properties.hasOwnProperty(key)) { 34 | continue; 35 | } 36 | if (properties[key] === '-') { 37 | properties[key] = ''; 38 | } 39 | if (key.includes('enable')) { 40 | properties[key] = strings.isTrue(properties[key]); 41 | } 42 | if (key.includes('disable')) { 43 | properties[key] = strings.isTrue(properties[key]); 44 | } 45 | if (booleanKeys.includes(key)) { 46 | properties[key] = strings.isTrue(properties[key]); 47 | } 48 | } 49 | return properties; 50 | } 51 | 52 | set = async (values: any) => { 53 | await requests.put(`/${this.group}`, values); 54 | } 55 | 56 | genRSAPrivateKey = async () => { 57 | let data = await requests.post(`/${this.group}/gen-rsa-private-key`); 58 | return data['key'] as string; 59 | } 60 | 61 | sendMail = async (values: any) => { 62 | await requests.post(`/${this.group}/send-mail`, values); 63 | } 64 | 65 | getLatestVersion = async () => { 66 | let data = await requests.get(`/${this.group}/latest-version?noerror`); 67 | return data as LatestVersion; 68 | } 69 | 70 | upgrade = async () => { 71 | await requests.post(`/${this.group}/upgrade`); 72 | } 73 | 74 | upgradeStatus = async () => { 75 | let data = await requests.get(`/${this.group}/upgrade-status`); 76 | return data as UpgradeStatus; 77 | } 78 | } 79 | 80 | let propertyApi = new PropertyApi(); 81 | export default propertyApi; -------------------------------------------------------------------------------- /src/api/role-api.ts: -------------------------------------------------------------------------------- 1 | import {Api} from "./core/api"; 2 | import requests from "./core/requests"; 3 | import {Menu} from "@/src/api/account-api"; 4 | 5 | export interface Role { 6 | id: string; 7 | name: string; 8 | type: string; 9 | createdAt: number; 10 | menus: Menu[]; 11 | } 12 | 13 | export interface TreeNode { 14 | key: string; 15 | title: string; 16 | value?: string; 17 | children?: TreeNode[]; 18 | isLeaf: boolean; 19 | } 20 | 21 | class RoleApi extends Api { 22 | constructor() { 23 | super("admin/roles"); 24 | } 25 | 26 | getMenus = async () => { 27 | return await requests.get(`/admin/menus`) as TreeNode[]; 28 | } 29 | } 30 | 31 | let roleApi = new RoleApi(); 32 | export default roleApi; -------------------------------------------------------------------------------- /src/api/scheduled-task-api.ts: -------------------------------------------------------------------------------- 1 | import {Api} from "./core/api"; 2 | import requests from "@/src/api/core/requests"; 3 | import qs from "qs"; 4 | 5 | export interface ScheduledTask { 6 | id: string; 7 | entryId: number; 8 | name: string; 9 | spec: string; 10 | type: string; 11 | assetIdList?: any; 12 | mode: string; 13 | script: string; 14 | enabled: boolean; 15 | createdAt: number; 16 | updatedAt: number; 17 | } 18 | 19 | export interface ScheduledTaskLog { 20 | id: string; 21 | jobId: string; 22 | jobType: string; 23 | results: []; 24 | createdAt: number; 25 | } 26 | 27 | export interface CheckStatusResult { 28 | name: string 29 | active: boolean 30 | usedTime: number 31 | usedTimeStr: string 32 | error: string 33 | } 34 | 35 | export interface ExecScriptResult { 36 | name: string 37 | success: boolean 38 | usedTime: number 39 | usedTimeStr: string 40 | result: string 41 | } 42 | 43 | class ScheduledTaskApi extends Api { 44 | constructor() { 45 | super("admin/scheduled-tasks"); 46 | } 47 | 48 | changeStatus = async (id: string, enabled: boolean) => { 49 | return await requests.post(`/${this.group}/${id}/change-status?enabled=${enabled}`); 50 | } 51 | 52 | exec = async (id: string) => { 53 | await requests.post(`/${this.group}/${id}/exec`) 54 | } 55 | 56 | getLogPaging = async (jobId: string, params: any) => { 57 | let paramsStr = qs.stringify(params); 58 | return await requests.get(`/${this.group}/${jobId}/logs/paging?${paramsStr}`); 59 | } 60 | 61 | getNextTenRuns = async (spec: string) => { 62 | return await requests.post(`/${this.group}/next-ten-runs`, { 63 | 'spec': spec 64 | }) as string[]; 65 | } 66 | } 67 | 68 | let scheduledTaskApi = new ScheduledTaskApi(); 69 | export default scheduledTaskApi; -------------------------------------------------------------------------------- /src/api/session-api.ts: -------------------------------------------------------------------------------- 1 | import {Api} from "@/src/api/core/api"; 2 | import requests from "@/src/api/core/requests"; 3 | 4 | export interface Session { 5 | region: string; 6 | id: string; 7 | clientIp: string; 8 | protocol: string; 9 | ip: string; 10 | port: number; 11 | username: string; 12 | assetId: string; 13 | assetName: string; 14 | userId: string; 15 | userAccount: string; 16 | status: string; 17 | connectedAt: number; 18 | disconnectedAt: number; 19 | connectionDuration: string; 20 | recording: string; 21 | recordingSize: number; 22 | commandCount: number; 23 | } 24 | 25 | export interface SessionWatermark { 26 | enabled: boolean 27 | content?: string[]; 28 | color?: string; 29 | size: number; 30 | } 31 | 32 | export interface SessionCommand { 33 | id: string; 34 | sessionId: string; 35 | riskLevel: number; 36 | command: string; 37 | result: string; 38 | createdAt: number; 39 | } 40 | 41 | export interface SessionSharer { 42 | ok: boolean 43 | url: string 44 | } 45 | 46 | class SessionApi extends Api { 47 | constructor() { 48 | super("admin/sessions"); 49 | } 50 | 51 | disconnect = async (sessionId: string) => { 52 | await requests.post(`/${this.group}/${sessionId}/disconnect`); 53 | } 54 | 55 | clear = async () => { 56 | await requests.post(`/${this.group}/clear`); 57 | } 58 | } 59 | 60 | const sessionApi = new SessionApi(); 61 | export default sessionApi; -------------------------------------------------------------------------------- /src/api/session-command-api.ts: -------------------------------------------------------------------------------- 1 | import {Api} from "@/src/api/core/api"; 2 | import {SessionCommand} from "@/src/api/session-api"; 3 | 4 | class SessionCommandApi extends Api { 5 | constructor() { 6 | super("admin/session-commands"); 7 | } 8 | } 9 | 10 | const sessionCommandApi = new SessionCommandApi(); 11 | export default sessionCommandApi; -------------------------------------------------------------------------------- /src/api/snippet-api.ts: -------------------------------------------------------------------------------- 1 | import {Api} from "./core/api"; 2 | 3 | export interface Snippet { 4 | id: string; 5 | name: string; 6 | content: string; 7 | createdBy: string; 8 | createdAt: number; 9 | } 10 | 11 | class SnippetApi extends Api{ 12 | constructor() { 13 | super("admin/snippets"); 14 | } 15 | } 16 | 17 | let snippetApi = new SnippetApi(); 18 | export default snippetApi; -------------------------------------------------------------------------------- /src/api/snippet-user-api.ts: -------------------------------------------------------------------------------- 1 | import {Api} from "./core/api"; 2 | import {Snippet} from "@/src/api/snippet-api"; 3 | 4 | class SnippetUserApi extends Api{ 5 | constructor() { 6 | super("portal/snippets"); 7 | } 8 | } 9 | 10 | let snippetUserApi = new SnippetUserApi(); 11 | export default snippetUserApi; -------------------------------------------------------------------------------- /src/api/ssh-gateway-api.ts: -------------------------------------------------------------------------------- 1 | import {Api} from "./core/api"; 2 | import requests from "@/src/api/core/requests"; 3 | 4 | export interface SSHGateway { 5 | id: string; 6 | type: string; 7 | name: string; 8 | ip: string; 9 | port: number; 10 | accountType: string; 11 | username: string; 12 | password: string; 13 | privateKey: string; 14 | passphrase: string; 15 | createdAt: number; 16 | status: string; 17 | statusMessage: string; 18 | } 19 | 20 | class SshGatewayApi extends Api { 21 | constructor() { 22 | super("admin/ssh-gateways"); 23 | } 24 | 25 | decrypt = async (id: string, securityToken: string) => { 26 | return await requests.get(`/${this.group}/${id}/decrypted?securityToken=${securityToken}`) as SSHGateway; 27 | } 28 | } 29 | 30 | let sshGatewayApi = new SshGatewayApi(); 31 | export default sshGatewayApi; -------------------------------------------------------------------------------- /src/api/storage-api.ts: -------------------------------------------------------------------------------- 1 | import {Api} from "./core/api"; 2 | import requests from "@/src/api/core/requests"; 3 | 4 | export interface Storage { 5 | id: string; 6 | name: string; 7 | isShare: boolean; 8 | limitSize: number; 9 | isDefault: boolean; 10 | createdBy: string; 11 | createdAt: number; 12 | usedSize: number; 13 | } 14 | 15 | class StorageApi extends Api { 16 | constructor() { 17 | super("admin/storages"); 18 | } 19 | 20 | getShares = async () => { 21 | return await requests.get(`/${this.group}/shares`) as Storage[]; 22 | } 23 | } 24 | 25 | let storageApi = new StorageApi(); 26 | export default storageApi; -------------------------------------------------------------------------------- /src/api/strategy-api.ts: -------------------------------------------------------------------------------- 1 | import {Api} from "@/src/api/core/api"; 2 | 3 | export interface Strategy { 4 | id: string; 5 | name: string; 6 | upload: boolean; 7 | download: boolean; 8 | delete: boolean; 9 | rename: boolean; 10 | edit: boolean; 11 | copy: boolean; 12 | paste: boolean; 13 | createDir: boolean; 14 | createFile: boolean; 15 | } 16 | 17 | class StrategyApi extends Api { 18 | constructor() { 19 | super("admin/strategies"); 20 | } 21 | } 22 | 23 | const strategyApi = new StrategyApi(); 24 | export default strategyApi; -------------------------------------------------------------------------------- /src/api/user-api.ts: -------------------------------------------------------------------------------- 1 | import {Api} from "./core/api"; 2 | import requests from "./core/requests"; 3 | 4 | export interface User { 5 | id: string; 6 | username: string; 7 | nickname: string; 8 | status: string; 9 | type: string; 10 | mail: string; 11 | source: string; 12 | createdAt: number; 13 | roles: string[]; 14 | } 15 | 16 | export interface LoginLog { 17 | id: string; 18 | username: string; 19 | clientIp: string; 20 | userAgent?: UserAgent; 21 | loginAt: number; 22 | success: boolean; 23 | reason: string; 24 | region: string; 25 | } 26 | 27 | export interface UserAgent { 28 | VersionNo: VersionNo; 29 | OSVersionNo: VersionNo; 30 | URL: string; 31 | String: string; 32 | Name: string; 33 | Version: string; 34 | OS: string; 35 | OSVersion: string; 36 | Device: string; 37 | Mobile: boolean; 38 | Tablet: boolean; 39 | Desktop: boolean; 40 | Bot: boolean; 41 | } 42 | 43 | export interface VersionNo { 44 | Major: number; 45 | Minor: number; 46 | Patch: number; 47 | } 48 | 49 | export interface CreateUserResult { 50 | id: string; 51 | nickname: string 52 | username: string 53 | password: string 54 | } 55 | 56 | class UserApi extends Api { 57 | constructor() { 58 | super("admin/users"); 59 | } 60 | 61 | resetTOTP = async (keys: string[]) => { 62 | await requests.post(`/${this.group}/reset-totp`, keys); 63 | } 64 | 65 | resetPassword = async (keys: string[], password?: string) => { 66 | let result = await requests.post(`/${this.group}/reset-password`, { 67 | 'keys': keys, 68 | 'password': password, 69 | }); 70 | return result['password']; 71 | } 72 | 73 | changeStatus = async (id: string, status: string) => { 74 | await requests.patch(`/${this.group}/${id}/status?status=${status}`); 75 | } 76 | 77 | // 不需要登录 78 | setupUser = async (values: any) => { 79 | await requests.post(`/setup-user`, values); 80 | } 81 | 82 | syncLdapUser = async () => { 83 | await requests.post(`/${this.group}/sync-from-ldap`); 84 | } 85 | 86 | import = async (file: File) => { 87 | let formData = new FormData(); 88 | formData.append("file", file); 89 | await requests.postForm(`/${this.group}/import`, formData); 90 | } 91 | } 92 | 93 | const userApi = new UserApi(); 94 | export default userApi; -------------------------------------------------------------------------------- /src/api/user-group-api.ts: -------------------------------------------------------------------------------- 1 | import {Api} from "./core/api"; 2 | import requests from "./core/requests"; 3 | 4 | export interface UserGroup { 5 | id: string 6 | name: string 7 | members: string[] | null 8 | createdAt: number 9 | } 10 | 11 | class UserGroupApi extends Api { 12 | constructor() { 13 | super("admin/user-groups"); 14 | } 15 | 16 | GetAll = async () => { 17 | return await requests.get(`/${this.group}`); 18 | } 19 | } 20 | 21 | const userGroupApi = new UserGroupApi(); 22 | export default userGroupApi; -------------------------------------------------------------------------------- /src/api/website-api.ts: -------------------------------------------------------------------------------- 1 | import {Api} from "@/src/api/core/api"; 2 | 3 | export interface Website { 4 | id: string; 5 | logo: string; 6 | name: string; 7 | enabled: boolean; 8 | targetUrl: string; 9 | targetHost: string; 10 | targetPort: number; 11 | domain: string; 12 | asciiDomain: string; 13 | entrance: string; 14 | description: string; 15 | status: string; 16 | statusText: string; 17 | agentGatewayId: string; 18 | basicAuth: BasicAuth; 19 | headers?: any; 20 | cert: Cert; 21 | public: Public; 22 | createdAt: number; 23 | 24 | scheme: string; 25 | host: string; 26 | port: number; 27 | 28 | outerUrl: string; 29 | } 30 | 31 | interface Public { 32 | enabled: boolean; 33 | ip: string; 34 | expiredAt: number; 35 | password: string; 36 | } 37 | 38 | interface Cert { 39 | enabled: boolean; 40 | cert: string; 41 | key: string; 42 | } 43 | 44 | interface BasicAuth { 45 | enabled: boolean; 46 | username: string; 47 | password: string; 48 | } 49 | 50 | class WebsiteApi extends Api { 51 | constructor() { 52 | super("admin/websites"); 53 | } 54 | } 55 | 56 | const websiteApi = new WebsiteApi(); 57 | export default websiteApi; -------------------------------------------------------------------------------- /src/assets/images/linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/images/linux.png -------------------------------------------------------------------------------- /src/assets/images/macos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/images/macos.png -------------------------------------------------------------------------------- /src/assets/images/windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/images/windows.png -------------------------------------------------------------------------------- /src/assets/os/alinux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/alinux.png -------------------------------------------------------------------------------- /src/assets/os/aliyun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/aliyun.png -------------------------------------------------------------------------------- /src/assets/os/amzn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/amzn.png -------------------------------------------------------------------------------- /src/assets/os/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/apple.png -------------------------------------------------------------------------------- /src/assets/os/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/arch.png -------------------------------------------------------------------------------- /src/assets/os/bytebase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/bytebase.png -------------------------------------------------------------------------------- /src/assets/os/centos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/centos.png -------------------------------------------------------------------------------- /src/assets/os/cloudcone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/cloudcone.png -------------------------------------------------------------------------------- /src/assets/os/cloudflare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/cloudflare.png -------------------------------------------------------------------------------- /src/assets/os/cmcc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/cmcc.png -------------------------------------------------------------------------------- /src/assets/os/ct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/ct.png -------------------------------------------------------------------------------- /src/assets/os/ctyun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/ctyun.png -------------------------------------------------------------------------------- /src/assets/os/cu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/cu.png -------------------------------------------------------------------------------- /src/assets/os/darwin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/darwin.png -------------------------------------------------------------------------------- /src/assets/os/debian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/debian.png -------------------------------------------------------------------------------- /src/assets/os/dsm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/dsm.png -------------------------------------------------------------------------------- /src/assets/os/esxi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/esxi.png -------------------------------------------------------------------------------- /src/assets/os/fedora.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/fedora.png -------------------------------------------------------------------------------- /src/assets/os/gcloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/gcloud.png -------------------------------------------------------------------------------- /src/assets/os/gcore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/gcore.png -------------------------------------------------------------------------------- /src/assets/os/ggy-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/ggy-icon.png -------------------------------------------------------------------------------- /src/assets/os/ggy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/ggy.png -------------------------------------------------------------------------------- /src/assets/os/kali.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/kali.png -------------------------------------------------------------------------------- /src/assets/os/linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/linux.png -------------------------------------------------------------------------------- /src/assets/os/opencloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/opencloud.png -------------------------------------------------------------------------------- /src/assets/os/opensuse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/opensuse.png -------------------------------------------------------------------------------- /src/assets/os/openwrt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/openwrt.png -------------------------------------------------------------------------------- /src/assets/os/pfsense.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/pfsense.png -------------------------------------------------------------------------------- /src/assets/os/pve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/pve.png -------------------------------------------------------------------------------- /src/assets/os/qcloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/qcloud.png -------------------------------------------------------------------------------- /src/assets/os/qnap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/qnap.png -------------------------------------------------------------------------------- /src/assets/os/redhat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/redhat.png -------------------------------------------------------------------------------- /src/assets/os/rocky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/rocky.png -------------------------------------------------------------------------------- /src/assets/os/synology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/synology.png -------------------------------------------------------------------------------- /src/assets/os/ubuntu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/ubuntu.png -------------------------------------------------------------------------------- /src/assets/os/ucloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/ucloud.png -------------------------------------------------------------------------------- /src/assets/os/unraid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/unraid.png -------------------------------------------------------------------------------- /src/assets/os/vm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/vm.png -------------------------------------------------------------------------------- /src/assets/os/vmb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/vmb.png -------------------------------------------------------------------------------- /src/assets/os/vmw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/vmw.png -------------------------------------------------------------------------------- /src/assets/os/vultr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/vultr.png -------------------------------------------------------------------------------- /src/assets/os/vyos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/vyos.png -------------------------------------------------------------------------------- /src/assets/os/win11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/win11.png -------------------------------------------------------------------------------- /src/assets/os/win7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/win7.png -------------------------------------------------------------------------------- /src/assets/os/windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/windows.png -------------------------------------------------------------------------------- /src/assets/os/zenlayer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/assets/os/zenlayer.png -------------------------------------------------------------------------------- /src/beautiful-scrollbar.css: -------------------------------------------------------------------------------- 1 | /* 定义滚动条整体样式 */ 2 | ::-webkit-scrollbar { 3 | width: 8px; 4 | height: 6px; 5 | } 6 | 7 | /* 定义滚动条轨道样式 */ 8 | ::-webkit-scrollbar-track { 9 | /* 使用内阴影模拟轨道样式 */ 10 | /*-webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.3) inset;*/ 11 | /* 注释掉的背景颜色,可根据需求启用 */ 12 | /* background-color: #F5F5F5; */ 13 | } 14 | 15 | /* 定义滚动条滑块样式 */ 16 | ::-webkit-scrollbar-thumb { 17 | /* 滑块圆角 */ 18 | border-radius: 3px; 19 | /* 移除默认的内阴影 */ 20 | -webkit-box-shadow: none !important; 21 | /* 滑块默认背景颜色 */ 22 | background-color: #90A2B9 !important; 23 | /* 移除默认的背景图像 */ 24 | background-image: none !important; 25 | } 26 | 27 | /* 定义滚动条滑块悬停时的样式 */ 28 | ::-webkit-scrollbar-thumb:hover { 29 | /* 悬停时滑块背景颜色 */ 30 | background-color: #45556C !important; 31 | } 32 | 33 | /* 定义文本选中时的样式,适用于 Mozilla 内核浏览器 */ 34 | ::-moz-selection { 35 | /* 选中背景透明 */ 36 | background: transparent; 37 | /* 选中文字颜色 */ 38 | color: #E2E8F1; 39 | } 40 | -------------------------------------------------------------------------------- /src/components/CpuProgressBar.tsx: -------------------------------------------------------------------------------- 1 | import {useMemo} from 'react'; 2 | import {Progress} from "antd"; 3 | 4 | const getStrokeColors = (user: number, system: number, steps: number) => { 5 | const greenSteps = Math.floor((user / 100) * steps); 6 | const redSteps = Math.floor((system / 100) * steps); 7 | const remainder = Math.max(steps - greenSteps - redSteps, 0); 8 | 9 | return Array(greenSteps).fill('#52c41a').concat( 10 | Array(redSteps).fill('#ff4d4f'), 11 | Array(remainder).fill('#d9d9d9') 12 | ); 13 | }; 14 | 15 | export const CpuProgressBar = ({cpu, index}) => { 16 | const steps = 50; 17 | const strokeColors = useMemo(() => getStrokeColors(cpu.user, cpu.system, steps), [cpu, steps]); 18 | 19 | return ( 20 |
21 | {percent.toFixed(1)}%} 27 | className="w-full" 28 | /> 29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/Disabled.tsx: -------------------------------------------------------------------------------- 1 | import React, {CSSProperties, useEffect, useRef} from 'react'; 2 | import {useTranslation} from "react-i18next"; 3 | 4 | export interface Props { 5 | disabled?: boolean; 6 | children?: React.ReactNode; 7 | classNames?: string[] 8 | style?: CSSProperties | undefined; 9 | } 10 | 11 | const Disabled = ({disabled, children, classNames, style}: Props) => { 12 | let ref = useRef(); 13 | let {t} = useTranslation(); 14 | 15 | useEffect(() => { 16 | if (disabled && ref.current) { 17 | let el = ref.current; 18 | el.querySelectorAll('*').forEach((e) => { 19 | e.setAttribute('disabled', 'true'); 20 | }); 21 | } 22 | }, [ref.current]); 23 | 24 | return ( 25 |
26 | {disabled && 27 |
28 |
29 | ⚠ {t('settings.license.restricted.label')}: 30 | {t('settings.license.restricted.content')} 31 | 34 | {t('settings.license.restricted.pay')} 35 | 36 |
37 |
38 | } 39 |
40 | {children} 41 |
42 |
43 | ); 44 | }; 45 | 46 | export default Disabled; -------------------------------------------------------------------------------- /src/components/ErrorPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Button, Result, Space} from "antd"; 3 | import {isRouteErrorResponse, Link, useNavigate, useRouteError} from "react-router-dom"; 4 | import {StyleProvider} from '@ant-design/cssinjs'; 5 | 6 | 7 | const ErrorPage = () => { 8 | 9 | const navigate = useNavigate(); 10 | const error = useRouteError(); 11 | 12 | let errorMessage: string; 13 | 14 | if (isRouteErrorResponse(error)) { 15 | // error is type `ErrorResponse` 16 | errorMessage = error.statusText; 17 | } else if (error instanceof Error) { 18 | errorMessage = error.message; 19 | } else if (typeof error === 'string') { 20 | errorMessage = error; 21 | } else { 22 | console.error(error); 23 | errorMessage = 'Unknown error'; 24 | } 25 | 26 | return ( 27 | 28 |
29 | 34 | 38 | 39 | 40 | } 41 | /> 42 |
43 |
44 | ); 45 | }; 46 | 47 | export default ErrorPage; -------------------------------------------------------------------------------- /src/components/Landing.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Spin} from "antd"; 3 | 4 | const Landing = () => { 5 | return ( 6 |
7 | 8 |
9 |
10 |
11 | ) 12 | }; 13 | 14 | export default Landing; -------------------------------------------------------------------------------- /src/components/NButton.tsx: -------------------------------------------------------------------------------- 1 | import React, {CSSProperties} from 'react'; 2 | import {Button} from "antd"; 3 | 4 | export interface NButtonProps { 5 | href?: string; 6 | target?: string; 7 | key?: string 8 | danger?: boolean; 9 | disabled?: boolean; 10 | children?: React.ReactNode; 11 | onClick?: () => void 12 | classNames?: string[] 13 | style?: CSSProperties | undefined; 14 | loading?: boolean 15 | } 16 | 17 | const NButton = ({classNames, href, target, danger, disabled, children, onClick, style, loading}: NButtonProps) => { 18 | // if (href) { 19 | // classNames?.push('underline'); 20 | // } 21 | 22 | if(!style){ 23 | style = {padding: 0} 24 | } 25 | return ( 26 | 40 | ); 41 | }; 42 | 43 | export default NButton; -------------------------------------------------------------------------------- /src/components/NLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from "react-router-dom"; 3 | 4 | interface Props { 5 | to: string; 6 | children?: React.ReactNode; 7 | } 8 | 9 | const NLink = ({to, children}: Props) => { 10 | return ( 11 | 12 | {children} 13 | 14 | ); 15 | }; 16 | 17 | export default NLink; -------------------------------------------------------------------------------- /src/components/NoMatch.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Button, Result, Space} from "antd"; 3 | import {Link, useNavigate} from "react-router-dom"; 4 | 5 | const NoMatch = () => { 6 | 7 | const navigate = useNavigate(); 8 | 9 | return ( 10 |
11 | 17 | 20 | 21 | 22 | 23 | } 24 | /> 25 |
26 | ); 27 | }; 28 | 29 | export default NoMatch; -------------------------------------------------------------------------------- /src/components/PromptModal.tsx: -------------------------------------------------------------------------------- 1 | import {ProForm, ProFormInstance, ProFormText} from '@ant-design/pro-components'; 2 | import {Modal} from 'antd'; 3 | import React, {useEffect, useRef} from 'react'; 4 | 5 | interface Props { 6 | title: string 7 | value?: string 8 | open: boolean 9 | onOk: (prompt: string) => void 10 | onCancel: () => void 11 | label: string 12 | placeholder: string 13 | confirmLoading: boolean 14 | } 15 | 16 | const PromptModal = ({title, open, onOk, onCancel, label, placeholder, confirmLoading, value}: Props) => { 17 | 18 | const formRef = useRef(); 19 | 20 | useEffect(() => { 21 | if (open) { 22 | formRef.current?.setFieldsValue({ 23 | 'prompt': value, 24 | }) 25 | } 26 | }, [open, value]); 27 | return ( 28 |
29 | { 33 | formRef.current?.validateFields() 34 | .then(async values => { 35 | onOk(values['prompt']); 36 | 37 | }); 38 | }} 39 | onCancel={() => { 40 | 41 | onCancel(); 42 | }} 43 | confirmLoading={confirmLoading} 44 | > 45 | 46 | 54 | 55 | 56 |
57 | ); 58 | }; 59 | 60 | export default PromptModal; -------------------------------------------------------------------------------- /src/components/Timeout.tsx: -------------------------------------------------------------------------------- 1 | import React, {Ref, useEffect, useImperativeHandle, useState} from 'react'; 2 | 3 | interface TimeoutProps { 4 | fn: () => void; 5 | ms: number; 6 | } 7 | 8 | export interface TimeoutHandle { 9 | reset: () => void; 10 | } 11 | 12 | const Timeout = React.forwardRef(({fn, ms}: TimeoutProps, ref: Ref) => { 13 | const [inactiveTime, setInactiveTime] = useState(0); 14 | 15 | useImperativeHandle(ref, () => ({ 16 | reset: () => { 17 | setInactiveTime(0); 18 | } 19 | })); 20 | 21 | useEffect(() => { 22 | if (ms <= 0) return; 23 | 24 | const interval = setInterval(() => { 25 | setInactiveTime(prev => { 26 | const newTime = prev + 1000; 27 | if (newTime >= ms) { 28 | fn(); 29 | clearInterval(interval); 30 | // console.log(`timeout`, new Date().getTime(), newTime, ms) 31 | return 0; // 重置计时器 32 | } 33 | // console.log(`still`, new Date().getTime(), newTime, ms) 34 | return newTime; 35 | }); 36 | }, 1000); 37 | 38 | return () => { 39 | clearInterval(interval); 40 | }; 41 | }, [ms, fn]); 42 | 43 | return ( 44 |
45 | 46 |
47 | ); 48 | }); 49 | 50 | export default Timeout; -------------------------------------------------------------------------------- /src/components/drag-weektime/DragWeekTime.css: -------------------------------------------------------------------------------- 1 | .week-time { 2 | min-width: 640px; 3 | position: relative; 4 | display: inline-block; 5 | } 6 | 7 | .schedule { 8 | background: #40a9ff; 9 | position: absolute; 10 | width: 0; 11 | height: 0; 12 | opacity: .6; 13 | pointer-events: none; 14 | } 15 | 16 | .schedule-notransi { 17 | transition: width .12s ease, height .12s ease, top .12s ease, left .12s ease; 18 | } 19 | 20 | .week-time-table { 21 | border-collapse: collapse; 22 | } 23 | 24 | .week-time-table th { 25 | vertical-align: inherit; 26 | font-weight: bold; 27 | } 28 | 29 | .week-time-table tr { 30 | height: 30px; 31 | } 32 | 33 | .week-time-table tr, 34 | .week-time-table td, 35 | .week-time-table th { 36 | user-select: none; 37 | border: 1px solid #d9d9d9; 38 | text-align: center; 39 | min-width: 12px; 40 | line-height: 1.8em; 41 | transition: background .2s ease; 42 | } 43 | 44 | /* Dark mode styles */ 45 | @media (prefers-color-scheme: dark) { 46 | .week-time-table tr, 47 | .week-time-table td, 48 | .week-time-table th { 49 | border: 1px solid #555555; /* 你可以根据需求调整颜色 */ 50 | } 51 | } 52 | 53 | .week-time-table .week-time-head { 54 | font-size: 12px; 55 | } 56 | 57 | .week-time-table .week-time-head .week-td { 58 | width: 70px; 59 | } 60 | 61 | .week-time-table .week-time-body { 62 | font-size: 12px; 63 | } 64 | 65 | .week-time-table .week-time-body td.ui-selected { 66 | background-color: #096dd9; 67 | } 68 | 69 | .week-time-table .week-time-preview { 70 | line-height: 2.4em; 71 | padding: 0 10px; 72 | font-size: 14px; 73 | } 74 | 75 | .week-time-table .week-time-preview .week-time-con { 76 | line-height: 46px; 77 | user-select: none; 78 | } 79 | 80 | .week-time-table .week-time-preview .week-time-time { 81 | text-align: left; 82 | line-height: 2.4em; 83 | } 84 | 85 | .week-time-table .week-time-preview .week-time-time p { 86 | max-width: 625px; 87 | line-height: 1.4em; 88 | word-break: break-all; 89 | margin-bottom: 8px; 90 | } 91 | 92 | .week-time-table tr, 93 | .week-time-table td, 94 | .week-time-table th { 95 | min-width: 12px; 96 | } 97 | 98 | .d-clearfix:after, 99 | .d-clearfix:before { 100 | clear: both; 101 | content: " "; 102 | display: table; 103 | } 104 | 105 | .g-pull-left { 106 | float: left; 107 | } 108 | 109 | .g-pull-right { 110 | float: right; 111 | margin-left: 5px; 112 | color: #096dd9; 113 | } 114 | 115 | .g-tip-text { 116 | color: #8c8c8c; 117 | margin-right: 10px; 118 | } -------------------------------------------------------------------------------- /src/components/time/times.ts: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | import strings from "@/src/utils/strings"; 3 | 4 | export const DateTime = `YYYY-MM-DD HH:mm:ss` 5 | 6 | class Times { 7 | format = (date?: dayjs.ConfigType, format?: dayjs.OptionType, locale?: string, strict?: boolean) => { 8 | return dayjs(date, format, locale, strict).format(DateTime) 9 | } 10 | 11 | formatTime = function formatTime(millis) { 12 | const totalSeconds = Math.floor(millis / 1000); 13 | 14 | // Split into seconds and minutes 15 | const seconds = totalSeconds % 60; 16 | const minutes = Math.floor(totalSeconds / 60); 17 | 18 | // Format seconds and minutes as MM:SS 19 | return strings.zeroPad(minutes, 2) + ':' + strings.zeroPad(seconds, 2); 20 | }; 21 | } 22 | 23 | let times = new Times(); 24 | export default times; -------------------------------------------------------------------------------- /src/helper/access-tab-channel.ts: -------------------------------------------------------------------------------- 1 | const channel = new BroadcastChannel('access-tab-sync'); 2 | 3 | export interface AccessTabSyncMessage { 4 | id: string; 5 | name: string; 6 | protocol: string; 7 | } 8 | 9 | export const accessAsset = (message: AccessTabSyncMessage) => { 10 | channel.postMessage(message); 11 | }; 12 | 13 | // const handler = (event: MessageEvent) => { 14 | // let message = event.data as AccessTabSyncMessage; 15 | // 16 | // }; 17 | 18 | // export const listenAccessAsset = (callback: (message: AccessTabSyncMessage) => void) => { 19 | // channel.addEventListener('message', (event)=>{ 20 | // callback && callback(event.data as AccessTabSyncMessage); 21 | // }); 22 | // }; 23 | // 24 | // export const unListenAccessAsset = () => { 25 | // channel.removeEventListener('message', handler); 26 | // }; -------------------------------------------------------------------------------- /src/helper/asset-helper.ts: -------------------------------------------------------------------------------- 1 | export const getImgColor = (protocol: string) => { 2 | switch (protocol) { 3 | case 'rdp': 4 | return `bg-purple-500`; 5 | case 'ssh': 6 | return `bg-green-500`; 7 | case 'telnet': 8 | return `bg-rose-500`; 9 | case 'vnc': 10 | return `bg-amber-500`; 11 | case 'kubernetes': 12 | return `bg-rose-500`; 13 | case 'http': 14 | return `bg-orange-500`; 15 | } 16 | } 17 | 18 | export const getProtocolColor = (protocol: string) => { 19 | switch (protocol) { 20 | case 'rdp': 21 | return `bg-purple-400`; 22 | case 'ssh': 23 | return `bg-green-400`; 24 | case 'telnet': 25 | return `bg-rose-400`; 26 | case 'vnc': 27 | return `bg-amber-400`; 28 | case 'kubernetes': 29 | return `bg-rose-400`; 30 | case 'http': 31 | return `bg-orange-400`; 32 | } 33 | } -------------------------------------------------------------------------------- /src/hook/atom.ts: -------------------------------------------------------------------------------- 1 | import {atom} from "jotai/index"; 2 | import {SimpleLicense} from "@/src/api/license-api"; 3 | 4 | export const atomWithLocalStorage = (key: string, initialValue: T) => { 5 | const getInitialValue = () => { 6 | const item = localStorage.getItem(key); 7 | if (item !== null) { 8 | return JSON.parse(item) as T; 9 | } 10 | return initialValue; 11 | }; 12 | 13 | const baseAtom = atom(getInitialValue()); 14 | 15 | return atom( 16 | (get) => get(baseAtom), 17 | (get, set, update: T | ((prev: T) => T)) => { 18 | const nextValue = typeof update === 'function' ? (update as (prev: T) => T)(get(baseAtom)) : update; 19 | set(baseAtom, nextValue); 20 | localStorage.setItem(key, JSON.stringify(nextValue)); 21 | }, 22 | ); 23 | }; 24 | 25 | export const atomLicenseWithLocalStorage = (key: string, initialValue: T) => { 26 | const getInitialValue = () => { 27 | const item = localStorage.getItem(key); 28 | if (item !== null) { 29 | const parsed = JSON.parse(item); 30 | // 如果 T 是 License 类型,手动转换为 License 实例 31 | if (initialValue instanceof SimpleLicense) { 32 | return new SimpleLicense(parsed.type, parsed.expired, parsed.oem) as T; 33 | } 34 | return parsed as T; 35 | } 36 | return initialValue; 37 | }; 38 | 39 | const baseAtom = atom(getInitialValue()); 40 | 41 | return atom( 42 | (get) => get(baseAtom), 43 | (get, set, update: T | ((prev: T) => T)) => { 44 | const nextValue = typeof update === 'function' ? (update as (prev: T) => T)(get(baseAtom)) : update; 45 | set(baseAtom, nextValue); 46 | localStorage.setItem(key, JSON.stringify(nextValue)); 47 | }, 48 | ); 49 | }; -------------------------------------------------------------------------------- /src/hook/title.ts: -------------------------------------------------------------------------------- 1 | import strings from "../utils/strings"; 2 | 3 | export const setTitle = (title: string | undefined) => { 4 | let titles = document.title.split('|'); 5 | if (strings.hasText(title)) { 6 | document.title = titles[0] + '|' + title; 7 | } else { 8 | document.title = titles[0]; 9 | } 10 | } -------------------------------------------------------------------------------- /src/hook/use-access-setting.ts: -------------------------------------------------------------------------------- 1 | import {atom, useAtom} from "jotai/index"; 2 | import {Setting} from "@/src/api/access-setting-api"; 3 | 4 | const configAtom = atom() 5 | 6 | export function useAccessSetting() { 7 | return useAtom(configAtom) 8 | } -------------------------------------------------------------------------------- /src/hook/use-access-size.ts: -------------------------------------------------------------------------------- 1 | import {atom, useAtom} from "jotai/index"; 2 | 3 | const configAtom = atom(0) 4 | 5 | export function useAccessContentSize() { 6 | return useAtom(configAtom) 7 | } -------------------------------------------------------------------------------- /src/hook/use-access-tab.ts: -------------------------------------------------------------------------------- 1 | import {atom, useAtom} from "jotai" 2 | 3 | 4 | const configAtom = atom('') 5 | 6 | export function useAccessTab() { 7 | return useAtom(configAtom) 8 | } -------------------------------------------------------------------------------- /src/hook/use-filesystem-id.ts: -------------------------------------------------------------------------------- 1 | import {atomWithLocalStorage} from "@/src/hook/atom"; 2 | import {useAtom} from "jotai/index"; 3 | 4 | const configAtom = atomWithLocalStorage('filesystem-id', ''); 5 | 6 | export function useFilesystemId() { 7 | return useAtom(configAtom); 8 | } -------------------------------------------------------------------------------- /src/hook/use-lang.ts: -------------------------------------------------------------------------------- 1 | import {atomWithLocalStorage} from "@/src/hook/atom"; 2 | import {useAtom} from "jotai"; 3 | import enUS from "antd/locale/en_US"; 4 | import zhCN from "antd/locale/zh_CN"; 5 | import zhTW from "antd/locale/zh_TW"; 6 | import jaJP from "antd/locale/ja_JP"; 7 | import i18n from "i18next"; 8 | 9 | const defaultLanguage = 'en-US'; 10 | 11 | const configAtom = atomWithLocalStorage('nt-language', defaultLanguage); 12 | 13 | export function useLang() { 14 | const [lang, setLang] = useAtom(configAtom); 15 | 16 | const wrapSetLang = (v: string) => { 17 | i18n.changeLanguage(v) 18 | .then(() => setLang(v)) 19 | .catch(error => console.error('Language change failed:', error)); 20 | } 21 | 22 | return [lang, wrapSetLang] as [string, (v: string) => void]; 23 | } 24 | 25 | export const translateI18nToAntdLocale = (key: string) => { 26 | switch (key) { 27 | case 'en-US': 28 | return enUS; 29 | case 'zh-CN': 30 | return zhCN; 31 | case 'zh-TW': 32 | return zhTW; 33 | case 'ja-JP': 34 | return jaJP; 35 | default: 36 | return zhCN; 37 | } 38 | } 39 | 40 | const VALID_LOCALES = ['en-US', 'zh-CN', 'zh-TW', 'ja-JP']; 41 | 42 | export const translateI18nToESLocale = (key: string) => { 43 | if (VALID_LOCALES.includes(key)) { 44 | return key; 45 | } 46 | return defaultLanguage; 47 | } -------------------------------------------------------------------------------- /src/hook/use-license.ts: -------------------------------------------------------------------------------- 1 | import {atomLicenseWithLocalStorage} from "@/src/hook/atom"; 2 | import {useAtom} from "jotai/index"; 3 | import {SimpleLicense} from "@/src/api/license-api"; 4 | 5 | // 创建初始 License 实例 6 | const initialLicense = new SimpleLicense(''); 7 | 8 | const configAtom = atomLicenseWithLocalStorage('nt-license', initialLicense); 9 | 10 | export function useLicense() { 11 | return useAtom(configAtom) 12 | } -------------------------------------------------------------------------------- /src/hook/use-terminal-theme.ts: -------------------------------------------------------------------------------- 1 | import {useAtom} from "jotai" 2 | import XtermThemes, {XtermTheme} from "@/src/color-theme/XtermThemes"; 3 | import {atomWithLocalStorage} from "@/src/hook/atom"; 4 | 5 | type ConfigTerminalTheme = { 6 | selected: string | null, 7 | theme?: XtermTheme 8 | fontSize: number, 9 | fontFamily: string, 10 | lineHeight: number, 11 | } 12 | 13 | const defaultTheme = `Apple System Colors` 14 | 15 | export const DefaultTerminalTheme = { 16 | selected: defaultTheme, 17 | theme: XtermThemes.filter(item => item.name === defaultTheme)[0], 18 | fontSize: 14, 19 | fontFamily: 'monaco, Consolas, "Lucida Console", monospace', 20 | lineHeight: 1.0, 21 | } 22 | 23 | const configAtom = atomWithLocalStorage('access-theme', DefaultTerminalTheme) 24 | 25 | export function useTerminalTheme() { 26 | return useAtom(configAtom) 27 | } 28 | 29 | export function CleanTheme(theme: ConfigTerminalTheme) { 30 | if (!theme.fontSize || theme.fontSize == 0) { 31 | theme.fontSize = DefaultTerminalTheme.fontSize 32 | } 33 | if (!theme.fontFamily) { 34 | theme.fontFamily = DefaultTerminalTheme.fontFamily 35 | } 36 | if (!theme.selected) { 37 | theme.selected = DefaultTerminalTheme.selected 38 | theme.theme = DefaultTerminalTheme.theme 39 | } 40 | if (!theme.lineHeight || theme.lineHeight == 0) { 41 | theme.lineHeight = DefaultTerminalTheme.lineHeight 42 | } 43 | return theme 44 | } -------------------------------------------------------------------------------- /src/hook/use-theme.ts: -------------------------------------------------------------------------------- 1 | import {atomWithLocalStorage} from "@/src/hook/atom"; 2 | import {useAtom} from "jotai/index"; 3 | import type {MapToken, SeedToken} from "antd/es/theme/interface"; 4 | import {theme} from "antd"; 5 | 6 | type ConfigTheme = { 7 | isDark: boolean 8 | algorithm: (token: SeedToken) => MapToken, 9 | backgroundColor?: string, 10 | } 11 | 12 | export const DefaultTheme: ConfigTheme = { 13 | isDark: false, 14 | algorithm: theme.defaultAlgorithm, 15 | backgroundColor: '#fff', 16 | } 17 | 18 | export const DarkTheme: ConfigTheme = { 19 | isDark: true, 20 | algorithm: theme.darkAlgorithm, 21 | backgroundColor: '#09090B', 22 | // backgroundColor: '#101217', 23 | } 24 | 25 | const configAtom = atomWithLocalStorage('nt-theme', DefaultTheme); 26 | 27 | export function useNTTheme() { 28 | return useAtom(configAtom) 29 | } -------------------------------------------------------------------------------- /src/hook/use-window-focus.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | const hasFocus = () => typeof document !== 'undefined' && document.hasFocus(); 4 | 5 | const useWindowFocus = () => { 6 | const [focused, setFocused] = useState(hasFocus); // Focus for first render 7 | 8 | useEffect(() => { 9 | setFocused(hasFocus()); // Focus for additional renders 10 | 11 | const onFocus = () => setFocused(true); 12 | const onBlur = () => setFocused(false); 13 | 14 | window.addEventListener('focus', onFocus); 15 | window.addEventListener('blur', onBlur); 16 | 17 | return () => { 18 | window.removeEventListener('focus', onFocus); 19 | window.removeEventListener('blur', onBlur); 20 | }; 21 | }, []); 22 | 23 | return focused; 24 | }; 25 | 26 | export default useWindowFocus; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 222.2 84% 4.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 222.2 84% 4.9%; 13 | --primary: 221.2 83.2% 53.3%; 14 | --primary-foreground: 210 40% 98%; 15 | --secondary: 210 40% 96.1%; 16 | --secondary-foreground: 222.2 47.4% 11.2%; 17 | --muted: 210 40% 96.1%; 18 | --muted-foreground: 215.4 16.3% 44%; 19 | --accent: 210 40% 96.1%; 20 | --accent-foreground: 222.2 47.4% 11.2%; 21 | --destructive: 0 72% 51%; 22 | --destructive-foreground: 210 40% 98%; 23 | --border: 214.3 31.8% 91.4%; 24 | --input: 214.3 31.8% 91.4%; 25 | --ring: 221.2 83.2% 53.3%; 26 | --radius: 0.5rem; 27 | --chart-1: 221.2 83.2% 53.3%; 28 | --chart-2: 212 95% 68%; 29 | --chart-3: 216 92% 60%; 30 | --chart-4: 210 98% 78%; 31 | --chart-5: 212 97% 87%; 32 | } 33 | 34 | .dark { 35 | --background: 240 10% 3.9%; 36 | --foreground: 0 0% 98%; 37 | --card: 240 10% 3.9%; 38 | --card-foreground: 0 0% 98%; 39 | --popover: 240 10% 3.9%; 40 | --popover-foreground: 0 0% 98%; 41 | --primary: 221.2 83.2% 53.3%; 42 | --primary-foreground: 210 40% 98%; 43 | --secondary: 210 40% 96.1%; 44 | --secondary-foreground: 222.2 47.4% 11.2%; 45 | --muted: 240 3.7% 15.9%; 46 | --muted-foreground: 240 5% 64.9%; 47 | --accent: 240 3.7% 15.9%; 48 | --accent-foreground: 0 0% 98%; 49 | --destructive: 0 72% 51%; 50 | --destructive-foreground: 210 40% 98%; 51 | --border: 240 3.7% 15.9%; 52 | --input: 240 3.7% 15.9%; 53 | --ring: 221.2 83.2% 53.3%; 54 | --chart-1: 221.2 83.2% 53.3%; 55 | --chart-2: 212 95% 68%; 56 | --chart-3: 216 92% 60%; 57 | --chart-4: 210 98% 78%; 58 | --chart-5: 212 97% 87%; 59 | } 60 | } 61 | 62 | 63 | @layer base { 64 | * { 65 | @apply border-border; 66 | } 67 | 68 | body { 69 | @apply bg-background text-foreground; 70 | } 71 | } -------------------------------------------------------------------------------- /src/layout/FooterComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Spin} from "antd"; 3 | import brandingApi from "../api/branding-api"; 4 | import {useQuery} from "@tanstack/react-query"; 5 | 6 | const FooterComponent = () => { 7 | 8 | let brandingQuery = useQuery({ 9 | queryKey: ['branding'], 10 | queryFn: brandingApi.getBranding, 11 | }); 12 | 13 | return ( 14 | 15 |
16 | {brandingQuery.data?.name}|{brandingQuery.data?.copyright}|{brandingQuery.data?.version} 17 |
18 |
19 | ); 20 | } 21 | 22 | export default FooterComponent; -------------------------------------------------------------------------------- /src/layout/ManagerLayout.css: -------------------------------------------------------------------------------- 1 | ::view-transition-old(root), 2 | ::view-transition-new(root) { 3 | animation: none; 4 | mix-blend-mode: normal; 5 | } 6 | -------------------------------------------------------------------------------- /src/layout/RedirectPage.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from 'react'; 2 | import Landing from "@/src/components/Landing"; 3 | import {useNavigate} from "react-router-dom"; 4 | import {useQuery} from "@tanstack/react-query"; 5 | import accountApi from "@/src/api/account-api"; 6 | import {isMobileByMediaQuery} from "@/src/utils/utils"; 7 | 8 | const RedirectPage = () => { 9 | 10 | let navigate = useNavigate(); 11 | 12 | let infoQuery = useQuery({ 13 | queryKey: ['infoQuery'], 14 | queryFn: () => { 15 | return accountApi.getUserInfo() 16 | }, 17 | }); 18 | 19 | useEffect(() => { 20 | if (!infoQuery.data) { 21 | return 22 | } 23 | 24 | let isMobile = isMobileByMediaQuery(); 25 | if (isMobile) { 26 | navigate('/x-asset'); 27 | return; 28 | } 29 | 30 | let data = infoQuery.data; 31 | if (data.type === 'user') { 32 | navigate('/x-asset'); 33 | } else { 34 | navigate('/dashboard'); 35 | } 36 | }, [infoQuery.data]); 37 | 38 | return ( 39 |
40 | 41 |
42 | ); 43 | }; 44 | 45 | export default RedirectPage; -------------------------------------------------------------------------------- /src/layout/UserLayout.css: -------------------------------------------------------------------------------- 1 | /*.header-shadow {*/ 2 | /* box-shadow: 0 1px 2px 0 rgba(0,0,0,.03),*/ 3 | /* 0 1px 6px -1px rgba(0,0,0,.02),*/ 4 | /* 0 2px 4px 0 rgba(0,0,0,.02);*/ 5 | /*}*/ 6 | 7 | .header-shadow { 8 | /*border: 2px solid #fff;*/ 9 | /*border-bottom: none;*/ 10 | /*box-shadow: -1px 1px 15px rgba(0, 21, 41, .15);*/ 11 | /*border-inline-end: 1px solid rgba(255, 255, 255, 0.06)*/ 12 | } 13 | 14 | .header-menu-selected { 15 | color: white; 16 | font-weight: bold; 17 | } 18 | 19 | .header-menu-selected:after { 20 | content: " "; 21 | border: none; 22 | /*border-radius: 2px;*/ 23 | border-bottom: 3px solid #FF5722; 24 | 25 | position: absolute; 26 | left: 0; 27 | width: 100%; 28 | bottom: 0; /* Adjust this value to position the underline correctly */ 29 | height: 3px; /* This should match the border-bottom thickness */ 30 | } -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import './index.css' 5 | import {QueryClient, QueryClientProvider} from "@tanstack/react-query"; 6 | import 'simplebar-react/dist/simplebar.min.css'; 7 | import relativeTime from 'dayjs/plugin/relativeTime'; 8 | import dayjs from "dayjs"; 9 | 10 | // 启用 relativeTime 插件 11 | dayjs.extend(relativeTime); 12 | 13 | const queryClient = new QueryClient(); 14 | 15 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 16 | // 17 | // 18 | // 19 | // 20 | // 21 | // 22 | 23 | 24 | 25 | ) 26 | -------------------------------------------------------------------------------- /src/pages/access/AccessPage.css: -------------------------------------------------------------------------------- 1 | .access-container .ant-tabs-nav{ 2 | margin: 0 !important; 3 | } 4 | 5 | .access-container .ant-tabs-nav-list .ant-tabs-tab{ 6 | border-radius: 0 !important; 7 | margin: 0 !important; 8 | border: none !important; 9 | background-color: transparent !important; 10 | /*padding-left: 4px !important;*/ 11 | /*padding-right: 8px !important;*/ 12 | border-bottom: 2px solid transparent !important; 13 | } 14 | 15 | .access-container .ant-tabs-nav-list .ant-tabs-tab-active { 16 | background-color: transparent !important; 17 | /*border-bottom-color: '#3C82F6' !important;*/ 18 | border-bottom: 2px solid #3C82F6 !important; 19 | 20 | } 21 | 22 | .access-container .ant-tabs-nav-list .ant-tabs-tab-active .ant-tabs-tab-btn{ 23 | color: #3C82F6 !important; 24 | } 25 | 26 | .access-container .ant-tabs-nav-list .ant-tabs-tab-btn{ 27 | color: #6B7280 !important; 28 | } -------------------------------------------------------------------------------- /src/pages/access/BrowserPage.css: -------------------------------------------------------------------------------- 1 | .loader { 2 | width: fit-content; 3 | height: fit-content; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | } 8 | 9 | .truckWrapper { 10 | width: 200px; 11 | height: 100px; 12 | display: flex; 13 | flex-direction: column; 14 | position: relative; 15 | align-items: center; 16 | justify-content: flex-end; 17 | overflow-x: hidden; 18 | } 19 | /* truck upper body */ 20 | .truckBody { 21 | width: 130px; 22 | height: fit-content; 23 | margin-bottom: 6px; 24 | animation: motion 1s linear infinite; 25 | } 26 | /* truck suspension animation*/ 27 | @keyframes motion { 28 | 0% { 29 | transform: translateY(0px); 30 | } 31 | 50% { 32 | transform: translateY(3px); 33 | } 34 | 100% { 35 | transform: translateY(0px); 36 | } 37 | } 38 | /* truck's tires */ 39 | .truckTires { 40 | width: 130px; 41 | height: fit-content; 42 | display: flex; 43 | align-items: center; 44 | justify-content: space-between; 45 | padding: 0px 10px 0px 15px; 46 | position: absolute; 47 | bottom: 0; 48 | } 49 | .truckTires svg { 50 | width: 24px; 51 | } 52 | 53 | .road { 54 | width: 100%; 55 | height: 1.5px; 56 | background-color: #282828; 57 | position: relative; 58 | bottom: 0; 59 | align-self: flex-end; 60 | border-radius: 3px; 61 | } 62 | .road::before { 63 | content: ""; 64 | position: absolute; 65 | width: 20px; 66 | height: 100%; 67 | background-color: #282828; 68 | right: -50%; 69 | border-radius: 3px; 70 | animation: roadAnimation 1.4s linear infinite; 71 | border-left: 10px solid white; 72 | } 73 | .road::after { 74 | content: ""; 75 | position: absolute; 76 | width: 10px; 77 | height: 100%; 78 | background-color: #282828; 79 | right: -65%; 80 | border-radius: 3px; 81 | animation: roadAnimation 1.4s linear infinite; 82 | border-left: 4px solid white; 83 | } 84 | 85 | .lampPost { 86 | position: absolute; 87 | bottom: 0; 88 | right: -90%; 89 | height: 90px; 90 | animation: roadAnimation 1.4s linear infinite; 91 | } 92 | 93 | @keyframes roadAnimation { 94 | 0% { 95 | transform: translateX(0px); 96 | } 97 | 100% { 98 | transform: translateX(-350px); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/pages/access/GuacClipboard.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import {Form, Input, Modal} from 'antd'; 3 | import {useTranslation} from 'react-i18next'; 4 | 5 | interface Props { 6 | open: boolean; 7 | clipboardText: string; 8 | handleOk: (clipboard: string) => void; 9 | handleCancel: () => void; 10 | } 11 | 12 | const GuacClipboard: React.FC = ({ 13 | open, 14 | clipboardText, 15 | handleOk, 16 | handleCancel, 17 | }) => { 18 | const {t} = useTranslation(); 19 | const [form] = Form.useForm(); 20 | const [confirmLoading, setConfirmLoading] = useState(false); 21 | 22 | // 当对话框打开或 clipboardText 改变时,重置表单并设置初始值 23 | useEffect(() => { 24 | if (open) { 25 | form.resetFields(); 26 | form.setFieldsValue({clipboard: clipboardText}); 27 | } 28 | }, [open, clipboardText, form]); 29 | 30 | // 点击“确定”时走 onFinish 流程 31 | const onFinish = (values: { clipboard: string }) => { 32 | setConfirmLoading(true); 33 | Promise.resolve() 34 | .then(() => handleOk(values.clipboard)) 35 | .finally(() => setConfirmLoading(false)); 36 | }; 37 | 38 | return ( 39 | form.submit()} 44 | onCancel={handleCancel} 45 | confirmLoading={confirmLoading} 46 | destroyOnHidden={true} // 关闭时销毁,避免多次打开时状态残留 47 | > 48 |
54 | 63 | 65 | 66 |
67 |
68 | ); 69 | }; 70 | 71 | export default GuacClipboard; 72 | -------------------------------------------------------------------------------- /src/pages/access/GuacdPlayback.css: -------------------------------------------------------------------------------- 1 | body { 2 | overflow: hidden; 3 | } 4 | 5 | .ctrl-bar { 6 | background: linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, #000000 25%, #000000 100%); 7 | color: #bbb; 8 | } -------------------------------------------------------------------------------- /src/pages/access/GuacdRequiredParameters.tsx: -------------------------------------------------------------------------------- 1 | import React, {useRef} from 'react'; 2 | import {Modal} from "antd"; 3 | import {useTranslation} from "react-i18next"; 4 | import {ProForm, ProFormInstance, ProFormText} from "@ant-design/pro-components"; 5 | 6 | interface Props { 7 | open: boolean 8 | parameters: string[] 9 | handleOk: (values: any) => void 10 | handleCancel: () => void 11 | confirmLoading: boolean 12 | } 13 | 14 | const GuacdRequiredParameters = ({open, parameters, confirmLoading, handleCancel, handleOk}: Props) => { 15 | 16 | let {t} = useTranslation(); 17 | const formRef = useRef(); 18 | return ( 19 | { 24 | formRef.current?.validateFields() 25 | .then(async values => { 26 | handleOk(values); 27 | 28 | }); 29 | }} 30 | confirmLoading={confirmLoading} 31 | onCancel={() => { 32 | 33 | handleCancel(); 34 | }} 35 | > 36 | 37 | {parameters?.map(parameter => { 38 | if (parameter == 'password') { 39 | return 40 | } else { 41 | return 42 | } 43 | })} 44 | 45 | 46 | ); 47 | }; 48 | 49 | export default GuacdRequiredParameters; -------------------------------------------------------------------------------- /src/pages/access/MobileAccessTerminal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {useSearchParams} from "react-router-dom"; 3 | import strings from "@/src/utils/strings"; 4 | import AccessTerminal from "@/src/pages/access/AccessTerminal"; 5 | 6 | const MobileAccessTerminal = () => { 7 | 8 | let [searchParams] = useSearchParams(); 9 | let assetId = searchParams.get('assetId'); 10 | 11 | if (!strings.hasText(assetId)) { 12 | return
13 | Error 14 |
15 | } 16 | 17 | return ( 18 |
19 | 20 |
21 | ); 22 | }; 23 | 24 | export default MobileAccessTerminal; -------------------------------------------------------------------------------- /src/pages/access/Terminal.ts: -------------------------------------------------------------------------------- 1 | export const MessageTypeData = 0; 2 | export const MessageTypeResize = 1; 3 | export const MessageTypeJoin = 2; 4 | export const MessageTypeExit = 3; 5 | export const MessageTypeDirChanged = 4; 6 | export const MessageTypeKeepAlive = 5; 7 | 8 | 9 | /** 10 | * package terminal 11 | * 12 | * import "strconv" 13 | * 14 | * type MessageType int 15 | * 16 | * const ( 17 | * MessageTypeData MessageType = 0 18 | * MessageTypeResize MessageType = 1 19 | * MessageTypeJoin MessageType = 2 20 | * MessageTypeExit MessageType = 3 21 | * ) 22 | * 23 | * type Message struct { 24 | * Type MessageType 25 | * Content string 26 | * } 27 | * 28 | * func (r Message) ToString() string { 29 | * if r.Content != "" { 30 | * return strconv.Itoa(int(r.Type)) + r.Content 31 | * } else { 32 | * return strconv.Itoa(int(r.Type)) 33 | * } 34 | * } 35 | * 36 | * func NewMessage(typ MessageType, content string) Message { 37 | * return Message{Content: content, Type: typ} 38 | * } 39 | * 40 | * func ParseMessage(value string) (message Message, err error) { 41 | * if value == "" { 42 | * return 43 | * } 44 | * 45 | * typ, err := strconv.Atoi(value[:1]) 46 | * if err != nil { 47 | * return 48 | * } 49 | * var content = value[1:] 50 | * message = NewMessage(MessageType(typ), content) 51 | * return 52 | * } 53 | * 54 | * type WindowSize struct { 55 | * Cols int `json:"cols"` 56 | * Rows int `json:"rows"` 57 | * } 58 | */ 59 | 60 | export class Message { 61 | public readonly type: number; 62 | public readonly content: string; 63 | 64 | constructor(type: number, content: string) { 65 | this.type = type; 66 | this.content = content; 67 | } 68 | 69 | public toString(): string { 70 | if (this.content !== "") { 71 | return this.type + this.content; 72 | } else { 73 | return this.type.toString(); 74 | } 75 | } 76 | 77 | public static parse(value: string): Message { 78 | if (value === "") { 79 | return new Message(0, ""); 80 | } 81 | 82 | let typ = parseInt(value[0]); 83 | let content = value.slice(1); 84 | return new Message(typ, content); 85 | } 86 | } -------------------------------------------------------------------------------- /src/pages/access/TerminalPlayback.css: -------------------------------------------------------------------------------- 1 | .ap-wrapper { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | } -------------------------------------------------------------------------------- /src/pages/access/guacamole/ControlButtons.tsx: -------------------------------------------------------------------------------- 1 | // components/ControlButtons.tsx 2 | import React from 'react'; 3 | import {Dropdown, FloatButton} from 'antd'; 4 | import { 5 | CopyOutlined, 6 | ExpandOutlined, 7 | FolderOutlined, 8 | ShareAltOutlined, 9 | ToolOutlined, 10 | WindowsOutlined 11 | } from '@ant-design/icons'; 12 | import {useTranslation} from 'react-i18next'; 13 | 14 | interface MenuItem { 15 | key: string; 16 | label: string; 17 | } 18 | 19 | interface Props { 20 | sessionId?: string; 21 | hasFileSystem?: boolean; 22 | onOpenFS: () => void; 23 | onShare: () => void; 24 | onClipboard: () => void; 25 | onFull: () => void; 26 | onSendKeys: (keys: string[]) => void; 27 | } 28 | 29 | const comboMenu: MenuItem[] = [ 30 | {key: '65507+65513+65535', label: 'Ctrl+Alt+Delete'}, 31 | {key: '65507+65513+65228', label: 'Ctrl+Alt+Backspace'}, 32 | {key: '65515+100', label: 'Window+D'}, 33 | {key: '65515+101', label: 'Window+E'}, 34 | {key: '65515+114', label: 'Window+R'}, 35 | {key: '65515+120', label: 'Window+X'}, 36 | {key: '65515', label: 'Window'}, 37 | // …other combos 38 | ]; 39 | 40 | const ControlButtons: React.FC = ({hasFileSystem, onOpenFS, onShare, onClipboard, onFull, onSendKeys}) => { 41 | const {t} = useTranslation(); 42 | const [open, setOpen] = React.useState(false); 43 | 44 | const handleMenuClick = (e: any) => { 45 | const keys = e.key.split('+'); 46 | onSendKeys(keys); 47 | }; 48 | 49 | return ( 50 | } 51 | onClick={() => setOpen(!open)}> 52 | {hasFileSystem && 53 | } tooltip={t('access.filesystem')} onClick={onOpenFS}/>} 54 | } tooltip={t('access.session.share.action')} onClick={onShare}/> 55 | } tooltip={t('access.clipboard')} onClick={onClipboard}/> 56 | 57 | } tooltip={t('access.combination_key')}/> 58 | 59 | } tooltip={t('access.toggle_full_screen')} onClick={onFull}/> 60 | 61 | ); 62 | }; 63 | 64 | export default ControlButtons; -------------------------------------------------------------------------------- /src/pages/access/guacamole/ErrorAlert.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {useTranslation} from 'react-i18next'; 3 | import {XCircle} from 'lucide-react'; 4 | import {motion} from 'framer-motion'; 5 | 6 | export interface GuacamoleStatus { 7 | code?: number | string; 8 | message?: string; 9 | } 10 | 11 | interface ErrorAlertProps { 12 | status?: GuacamoleStatus; 13 | onReconnect?: () => void; 14 | } 15 | 16 | export const ErrorAlert: React.FC = ({status, onReconnect}) => { 17 | 18 | const {t} = useTranslation(); 19 | 20 | return ( 21 | 27 |
28 | 29 |

30 | {t('access.guacamole.error_title', 'Connection Error')} 31 |

32 |
33 | 34 | {status?.code && ( 35 |
36 | {t('access.guacamole.code', 'Code')}: 37 | {status.code} 38 |
39 | )} 40 | 41 | {status?.message && ( 42 |
43 | {t('access.guacamole.message', 'Message')}: 44 | {status.message} 45 |
46 | )} 47 | 48 | {onReconnect && 49 | 55 | } 56 | 57 |
58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /src/pages/access/guacamole/RenderState.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {HashLoader} from 'react-spinners'; 3 | import {ErrorAlert, GuacamoleStatus} from '@/src/pages/access/guacamole/ErrorAlert'; 4 | import {useTranslation} from 'react-i18next'; 5 | 6 | interface Props { 7 | state?: number; 8 | status?: GuacamoleStatus; 9 | onReconnect?: () => void; 10 | } 11 | 12 | export const GUACAMOLE_STATE_IDLE = 0; 13 | export const GUACAMOLE_STATE_CONNECTING = 1; 14 | export const GUACAMOLE_STATE_WAITING = 2; 15 | export const GUACAMOLE_STATE_CONNECTED = 3; 16 | export const GUACAMOLE_STATE_DISCONNECTING = 4; 17 | export const GUACAMOLE_STATE_DISCONNECTED = 5; 18 | 19 | const RenderState: React.FC = ({state, status, onReconnect}) => { 20 | const {t} = useTranslation(); 21 | if (state === GUACAMOLE_STATE_CONNECTED) return null; 22 | 23 | const loading = state !== GUACAMOLE_STATE_DISCONNECTED; 24 | const labels = { 25 | STATE_IDLE: t('guacamole.state.idle'), 26 | STATE_CONNECTING: t('guacamole.state.connecting'), 27 | STATE_WAITING: t('guacamole.state.waiting'), 28 | STATE_CONNECTED: t('guacamole.state.connected'), 29 | STATE_DISCONNECTING: t('guacamole.state.disconnecting'), 30 | STATE_DISCONNECTED: t('guacamole.state.disconnected'), 31 | }; 32 | 33 | return ( 34 |
35 |
36 | {loading ? ( 37 |
38 | 39 |
{labels[state!]}
40 |
41 | ) : ( 42 | 43 | )} 44 |
45 |
46 | ); 47 | }; 48 | 49 | export default RenderState; -------------------------------------------------------------------------------- /src/pages/access/guacamole/keys.ts: -------------------------------------------------------------------------------- 1 | 2 | export const duplicateKeys = new Map([ 3 | // 数字键 4 | [48, 65456], // '0' → KP_0 5 | [49, 65457], // '1' → KP_1 6 | [50, 65458], // '2' → KP_2 7 | [51, 65459], // '3' → KP_3 8 | [52, 65460], // '4' → KP_4 9 | [53, 65461], // '5' → KP_5 10 | [54, 65462], // '6' → KP_6 11 | [55, 65463], // '7' → KP_7 12 | [56, 65464], // '8' → KP_8 13 | [57, 65465], // '9' → KP_9 14 | 15 | // 运算符 16 | [42, 65450], // '*' → KP_Multiply 17 | [43, 65451], // '+' → KP_Add 18 | [45, 65453], // '-' → KP_Subtract 19 | [47, 65455], // '/' → KP_Divide 20 | 21 | // 标点符号 22 | [46, 65454], // '.' → KP_Decimal 23 | [44, 65452], // ',' → KP_Separator(小键盘逗号,部分区域布局) 24 | 25 | // 反向映射(确保双向判断) 26 | [65456, 48], [65457, 49], [65458, 50], [65459, 51], [65460, 52], 27 | [65461, 53], [65462, 54], [65463, 55], [65464, 56], [65465, 57], 28 | [65450, 42], [65451, 43], [65453, 45], [65455, 47], 29 | [65454, 46], [65452, 44], 30 | ]); -------------------------------------------------------------------------------- /src/pages/account/AccessToken.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Button, Descriptions, Space, Typography} from "antd"; 3 | import {useTranslation} from "react-i18next"; 4 | import {useMutation, useQuery} from "@tanstack/react-query"; 5 | import accountApi from "@/src/api/account-api"; 6 | import times from "@/src/components/time/times"; 7 | 8 | const AccessToken = () => { 9 | let {t} = useTranslation(); 10 | 11 | let tokenQuery = useQuery({ 12 | queryKey: ['access-token'], 13 | queryFn: accountApi.getAccessToken, 14 | }); 15 | 16 | let tokenMutation = useMutation({ 17 | mutationFn: accountApi.createAccessToken, 18 | onSuccess: () => { 19 | tokenQuery.refetch(); 20 | } 21 | }); 22 | 23 | return ( 24 |
25 | {t('account.access_token')} 26 | 27 | 28 | {tokenQuery.data?.id} 29 | 30 | 31 | {times.format(tokenQuery.data?.createdAt)} 32 | 33 | 34 | 35 | 36 | 40 | 41 |
42 | ); 43 | }; 44 | 45 | export default AccessToken; -------------------------------------------------------------------------------- /src/pages/account/ChangeInfo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {ProForm, ProFormText, ProFormTextArea} from "@ant-design/pro-components"; 3 | import {useTranslation} from "react-i18next"; 4 | import {App, Typography} from "antd"; 5 | import accountApi, {AccountInfo} from "@/src/api/account-api"; 6 | 7 | const {Title} = Typography; 8 | 9 | const ChangeInfo = () => { 10 | let {t} = useTranslation(); 11 | 12 | let {message} = App.useApp(); 13 | 14 | const get = async () => { 15 | return await accountApi.getUserInfo(); 16 | } 17 | 18 | const set = async (info: AccountInfo) => { 19 | await accountApi.changeInfo(info); 20 | message.success(t('general.success')); 21 | return true 22 | } 23 | 24 | return ( 25 |
26 | {t('account.change.info')} 27 |
28 | 29 | 30 | 33 | 34 |
35 | ); 36 | }; 37 | 38 | export default ChangeInfo; -------------------------------------------------------------------------------- /src/pages/account/InfoPage.tsx: -------------------------------------------------------------------------------- 1 | import {Layout, Tabs} from 'antd'; 2 | import React from 'react'; 3 | import ChangePassword from "./ChangePassword"; 4 | import OTP from "./OTP"; 5 | import {useTranslation} from "react-i18next"; 6 | import AccessToken from "@/src/pages/account/AccessToken"; 7 | import {useSearchParams} from "react-router-dom"; 8 | import {maybe} from "@/src/utils/maybe"; 9 | import ChangeInfo from "@/src/pages/account/ChangeInfo"; 10 | import {isMobileByMediaQuery} from "@/src/utils/utils"; 11 | import {cn} from "@/lib/utils"; 12 | import Passkey from "@/src/pages/account/Passkey"; 13 | 14 | const InfoPage = () => { 15 | 16 | let {t} = useTranslation(); 17 | 18 | const [searchParams, setSearchParams] = useSearchParams(); 19 | 20 | let activeKey = maybe(searchParams.get('activeKey'), "change-info"); 21 | const handleTagChange = (key: string) => { 22 | setSearchParams({'activeKey': key}); 23 | } 24 | 25 | const items = [ 26 | { 27 | label: t('account.change.info'), 28 | key: 'change-info', 29 | children: 30 | }, 31 | { 32 | label: t('account.change.password'), 33 | key: 'change-password', 34 | children: 35 | }, 36 | { 37 | label: t('account.otp'), 38 | key: 'otp', 39 | children: 40 | }, 41 | { 42 | label: t('account.passkey'), 43 | key: 'passkey', 44 | children: 45 | }, 46 | { 47 | label: t('account.access_token'), 48 | key: 'access-token', 49 | children: 50 | }, 51 | ]; 52 | 53 | let isMobile = isMobileByMediaQuery(); 54 | 55 | return ( 56 | <> 57 | 61 | 67 | 68 | 69 | 70 | 71 | ); 72 | }; 73 | 74 | export default InfoPage; -------------------------------------------------------------------------------- /src/pages/account/OTP.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import {Typography} from "antd"; 3 | import accountApi from "../../api/account-api"; 4 | import {useQuery} from "@tanstack/react-query"; 5 | import OTPBinding from "./OTPBinding"; 6 | import OTPUnBinding from "./OTPUnBinding"; 7 | import {useTranslation} from "react-i18next"; 8 | 9 | const OTP = () => { 10 | 11 | let {t} = useTranslation(); 12 | let infoQuery = useQuery({ 13 | queryKey: ['info'], 14 | queryFn: accountApi.getUserInfo, 15 | }) 16 | 17 | let [view, setView] = useState('binding'); 18 | 19 | useEffect(() => { 20 | if (infoQuery.data?.enabledTotp) { 21 | setView('unbinding'); 22 | }else { 23 | setView('binding'); 24 | } 25 | }, [infoQuery.data]); 26 | 27 | const refetch = () => { 28 | infoQuery.refetch(); 29 | } 30 | 31 | const renderView = (view: string) => { 32 | switch (view) { 33 | case 'unbinding': 34 | return ; 35 | case 'binding': 36 | return ; 37 | } 38 | } 39 | 40 | return ( 41 |
42 | {t('account.otp')} 43 | {renderView(view)} 44 |
45 | ); 46 | }; 47 | 48 | export default OTP; -------------------------------------------------------------------------------- /src/pages/account/OTPBinding.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {useQuery} from "@tanstack/react-query"; 3 | import accountApi from "../../api/account-api"; 4 | import {Alert, App, Button, Form, Input, message, QRCode, Space, Typography} from "antd"; 5 | import {useTranslation} from "react-i18next"; 6 | 7 | export interface Binding2faProps { 8 | refetch: () => void 9 | } 10 | 11 | const OTPBinding = ({refetch}: Binding2faProps) => { 12 | 13 | const [form] = Form.useForm(); 14 | let {t} = useTranslation(); 15 | let {message} = App.useApp(); 16 | 17 | let totpQuery = useQuery({ 18 | queryKey: ['totp'], 19 | queryFn: accountApi.reloadTotp, 20 | refetchOnWindowFocus: false, 21 | }) 22 | 23 | const confirmTOTP = async (values: any) => { 24 | values['secret'] = totpQuery.data?.secret; 25 | await accountApi.confirmTotp(values); 26 | message.success(t('general.success')); 27 | refetch(); 28 | } 29 | 30 | const renderQRCodeStatus = () => { 31 | if (totpQuery.isLoading) { 32 | return "loading"; 33 | } 34 | // if(expired === true){ 35 | // return 'expired'; 36 | // } 37 | return "active" 38 | } 39 | 40 | return ( 41 |
42 | 43 | totpQuery.refetch()}/> 45 | 49 |
50 | 54 | 55 | 56 | 57 | 60 | 61 |
62 |
63 | 64 |
65 | ); 66 | }; 67 | 68 | export default OTPBinding; -------------------------------------------------------------------------------- /src/pages/account/OTPUnBinding.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import accountApi from "../../api/account-api"; 3 | import {Button, message, Modal, Result} from "antd"; 4 | import {ExclamationCircleOutlined} from "@ant-design/icons"; 5 | import {useTranslation} from "react-i18next"; 6 | 7 | export interface UnBinding2faProps { 8 | refetch: () => void 9 | } 10 | 11 | const OTPUnBinding = ({refetch}: UnBinding2faProps) => { 12 | let {t} = useTranslation(); 13 | return ( 14 |
15 | { 21 | Modal.confirm({ 22 | title: t('account.otp_unbind_title'), 23 | icon: , 24 | content: t('account.otp_unbind_subtitle'), 25 | okType: 'danger', 26 | onOk: async () => { 27 | let success = await accountApi.resetTotp(); 28 | if (success) { 29 | message.success(t('general.success')); 30 | refetch(); 31 | } 32 | }, 33 | }) 34 | }}> 35 | {t('account.otp_unbind')} 36 | , 37 | ]} 38 | /> 39 |
40 | ); 41 | }; 42 | 43 | export default OTPUnBinding; -------------------------------------------------------------------------------- /src/pages/account/PasskeyModal.tsx: -------------------------------------------------------------------------------- 1 | import React, {useRef} from 'react'; 2 | import {Modal} from "antd"; 3 | import {useTranslation} from "react-i18next"; 4 | import {ProForm, ProFormInstance, ProFormText} from "@ant-design/pro-components"; 5 | import {WebauthnCredential} from "@/src/api/account-api"; 6 | 7 | export interface Props { 8 | open: boolean 9 | handleOk: (values: any) => void 10 | handleCancel: () => void 11 | confirmLoading: boolean 12 | credential: WebauthnCredential 13 | } 14 | 15 | const PasskeyModal = ({ 16 | open, 17 | handleOk, 18 | handleCancel, 19 | confirmLoading, 20 | credential, 21 | }: Props) => { 22 | 23 | let {t} = useTranslation(); 24 | const formRef = useRef(); 25 | const get = async () => { 26 | return credential; 27 | } 28 | 29 | return ( 30 | { 36 | formRef.current?.validateFields() 37 | .then(async values => { 38 | handleOk(values); 39 | 40 | }); 41 | }} 42 | onCancel={() => { 43 | 44 | handleCancel(); 45 | }} 46 | confirmLoading={confirmLoading} 47 | > 48 | 49 | 52 | 53 | ); 54 | }; 55 | 56 | export default PasskeyModal; -------------------------------------------------------------------------------- /src/pages/assets/AssetDetail.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {useParams, useSearchParams} from "react-router-dom"; 3 | import {Tabs} from "antd"; 4 | import {maybe} from "@/src/utils/maybe"; 5 | import AssetsPost from "@/src/pages/assets/AssetPost"; 6 | import AssetAuthorised from "@/src/pages/authorised/AssetAuthorised"; 7 | import {useTranslation} from "react-i18next"; 8 | 9 | 10 | const AssetDetail = () => { 11 | let {t} = useTranslation(); 12 | 13 | let params = useParams(); 14 | const id = params['assetId'] as string; 15 | const [searchParams, setSearchParams] = useSearchParams(); 16 | let key = maybe(searchParams.get('activeKey'), "info"); 17 | 18 | let [activeKey, setActiveKey] = useState(key); 19 | 20 | const handleTagChange = (key: string) => { 21 | setActiveKey(key); 22 | setSearchParams({'activeKey': key}); 23 | } 24 | 25 | return ( 26 |
27 | 33 | }, 34 | { 35 | key: 'bind-user', 36 | label: t('assets.bind_user'), 37 | children: 38 | }, 39 | { 40 | key: 'bind-user-group', 41 | label: t('assets.bind_user_group'), 42 | children: 44 | }, 45 | ]} 46 | > 47 | 48 |
49 | ); 50 | }; 51 | 52 | export default AssetDetail; -------------------------------------------------------------------------------- /src/pages/assets/AssetPostDrawer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Drawer} from "antd"; 3 | import AssetsPost from "@/src/pages/assets/AssetPost"; 4 | import {useTranslation} from "react-i18next"; 5 | 6 | interface Props { 7 | open: boolean; 8 | onClose: () => void; 9 | 10 | assetId?: string; 11 | groupId?: string; 12 | copy?: boolean; 13 | } 14 | 15 | const AssetPostDrawer = ({open, onClose, assetId, groupId, copy}: Props) => { 16 | let {t} = useTranslation(); 17 | 18 | let title = assetId ? t('actions.edit') : t('actions.new') 19 | if(copy){ 20 | title = t('actions.copy') 21 | } 22 | return ( 23 |
24 | 30 | 36 | 37 |
38 | ); 39 | }; 40 | 41 | export default AssetPostDrawer; -------------------------------------------------------------------------------- /src/pages/assets/AssetTreeChoose.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import assetApi from "@/src/api/asset-api"; 3 | import {useTranslation} from "react-i18next"; 4 | import {Drawer, Tree, TreeDataNode} from "antd"; 5 | import {useQuery} from "@tanstack/react-query"; 6 | 7 | interface Props { 8 | assetIds: string[]; 9 | open: boolean; 10 | onClose: () => void; 11 | } 12 | 13 | const AssetTreeChoose = ({assetIds, open, onClose}: Props) => { 14 | let {t} = useTranslation(); 15 | 16 | const [treeData, setTreeData] = useState([]); 17 | let [expandedKeys, setExpandedKeys] = useState([]); 18 | let [selectedKey, setSelectedKey] = useState(''); 19 | 20 | let query = useQuery({ 21 | queryKey: ['assets/groups'], 22 | queryFn: assetApi.getGroups, 23 | }); 24 | 25 | useEffect(() => { 26 | if (open) { 27 | query.refetch(); 28 | } else { 29 | setSelectedKey(''); 30 | } 31 | }, [open]); 32 | 33 | useEffect(() => { 34 | if (query.data) { 35 | setTreeData(query.data); 36 | let keys1 = getAllKeys(query.data); 37 | setExpandedKeys(keys1); 38 | } 39 | }, [query.data]); 40 | 41 | const getAllKeys = (data: TreeDataNode[]) => { 42 | let keys = []; 43 | data.forEach((item) => { 44 | keys.push(item.key); 45 | if (item.children) { 46 | keys = keys.concat(getAllKeys(item.children)); 47 | } 48 | }); 49 | return keys; 50 | }; 51 | 52 | const post = (groupId: string) => { 53 | assetApi.changeGroup({ 54 | assetIds: assetIds, 55 | groupId: groupId, 56 | }) 57 | .then(() => { 58 | onClose(); 59 | }) 60 | } 61 | 62 | return ( 63 |
64 | 67 | { 79 | if (keys && keys.length > 0) { 80 | setSelectedKey(keys[0] as string); 81 | post(keys[0] as string); 82 | } 83 | }} 84 | /> 85 | 86 |
87 | ); 88 | }; 89 | 90 | export default AssetTreeChoose; -------------------------------------------------------------------------------- /src/pages/assets/CertificateIssuedLog.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useRef, useState} from 'react'; 2 | import {Drawer} from "antd"; 3 | import {baseUrl, getToken} from "@/src/api/core/requests"; 4 | import {useTranslation} from "react-i18next"; 5 | 6 | interface Props { 7 | open: boolean; 8 | onClose: () => void 9 | } 10 | 11 | const CertificateIssuedLog = ({open, onClose}: Props) => { 12 | 13 | let {t} = useTranslation(); 14 | const [logs, setLogs] = useState([]); 15 | const bottomRef = useRef(null); 16 | 17 | useEffect(() => { 18 | if (!open) { 19 | return; 20 | } 21 | const eventSource = new EventSource(`${baseUrl()}/admin/certificates/issued/log?X-Auth-Token=${getToken()}`); 22 | 23 | eventSource.onmessage = (event) => { 24 | setLogs((prevLogs) => [...prevLogs, event.data]); 25 | }; 26 | 27 | eventSource.onerror = (err) => { 28 | console.error("SSE connection error:", err); 29 | eventSource.close(); 30 | }; 31 | 32 | return () => { 33 | eventSource.close(); 34 | }; 35 | }, [open]); 36 | 37 | useEffect(() => { 38 | bottomRef.current?.scrollIntoView({behavior: "smooth"}); 39 | }, [logs]); 40 | 41 | 42 | return ( 43 | 48 |
60 | {logs.map((log, idx) => ( 61 |
{log}
62 | ))} 63 |
64 |
65 | 66 | ); 67 | }; 68 | 69 | export default CertificateIssuedLog; -------------------------------------------------------------------------------- /src/pages/assets/SnippetModal.tsx: -------------------------------------------------------------------------------- 1 | import React, {useRef} from 'react'; 2 | import {Modal} from "antd"; 3 | import snippetApi from "../../api/snippet-api"; 4 | import {ProForm, ProFormInstance, ProFormText, ProFormTextArea} from "@ant-design/pro-components"; 5 | import {useTranslation} from "react-i18next"; 6 | 7 | const api = snippetApi; 8 | 9 | export interface SnippetProps { 10 | open: boolean 11 | handleOk: (values: any) => void 12 | handleCancel: () => void 13 | confirmLoading: boolean 14 | id: string | undefined 15 | } 16 | 17 | const SnippetModal = ({ 18 | open, 19 | handleOk, 20 | handleCancel, 21 | confirmLoading, 22 | id, 23 | }: SnippetProps) => { 24 | let {t} = useTranslation(); 25 | const formRef = useRef(); 26 | 27 | const get = async () => { 28 | if (id) { 29 | return await api.getById(id); 30 | } 31 | return {}; 32 | } 33 | 34 | return ( 35 | 36 | { 42 | formRef.current?.validateFields() 43 | .then(async values => { 44 | handleOk(values); 45 | 46 | }); 47 | }} 48 | onCancel={() => { 49 | 50 | handleCancel(); 51 | }} 52 | confirmLoading={confirmLoading} 53 | > 54 | 55 | 59 | 60 | ) 61 | }; 62 | 63 | export default SnippetModal; -------------------------------------------------------------------------------- /src/pages/assets/StorageModal.tsx: -------------------------------------------------------------------------------- 1 | import React, {useRef} from 'react'; 2 | import {Modal} from "antd"; 3 | import {ProForm, ProFormDigit, ProFormInstance, ProFormSwitch, ProFormText} from "@ant-design/pro-components"; 4 | import {useTranslation} from "react-i18next"; 5 | import storageApi from "@/src/api/storage-api"; 6 | 7 | const api = storageApi; 8 | 9 | export interface SnippetProps { 10 | open: boolean 11 | handleOk: (values: any) => void 12 | handleCancel: () => void 13 | confirmLoading: boolean 14 | id: string | undefined 15 | } 16 | 17 | const StorageModal = ({ 18 | open, 19 | handleOk, 20 | handleCancel, 21 | confirmLoading, 22 | id, 23 | }: SnippetProps) => { 24 | let {t} = useTranslation(); 25 | const formRef = useRef(); 26 | 27 | const get = async () => { 28 | if (id) { 29 | let data = await api.getById(id); 30 | if (data.limitSize > 0) { 31 | data.limitSize = data.limitSize / 1024 / 1024 / 1024; 32 | } 33 | return await data; 34 | } 35 | return { 36 | 37 | }; 38 | } 39 | 40 | return ( 41 | 42 | { 48 | formRef.current?.validateFields() 49 | .then(async values => { 50 | values['limitSize'] = values['limitSize'] * 1024 * 1024 * 1024; 51 | handleOk(values); 52 | 53 | }); 54 | }} 55 | onCancel={() => { 56 | 57 | handleCancel(); 58 | }} 59 | confirmLoading={confirmLoading} 60 | > 61 | 62 | 75 | 76 | ) 77 | }; 78 | 79 | export default StorageModal; -------------------------------------------------------------------------------- /src/pages/assets/WebsiteDetail.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {useParams, useSearchParams} from "react-router-dom"; 3 | import {Tabs} from "antd"; 4 | import {maybe} from "@/src/utils/maybe"; 5 | import WebsiteAuthorised from "@/src/pages/assets/WebsiteAuthorised"; 6 | 7 | const {TabPane} = Tabs; 8 | 9 | const WebsiteDetail = () => { 10 | let params = useParams(); 11 | const id = params['websiteId'] as string; 12 | const [searchParams, setSearchParams] = useSearchParams(); 13 | let key = maybe(searchParams.get('activeKey'), "bind-user"); 14 | 15 | let [activeKey, setActiveKey] = useState(key); 16 | 17 | const handleTagChange = (key: string) => { 18 | setActiveKey(key); 19 | setSearchParams({'activeKey': key}); 20 | } 21 | 22 | return ( 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | ); 34 | }; 35 | 36 | export default WebsiteDetail; -------------------------------------------------------------------------------- /src/pages/assets/WebsiteInfo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface Props { 4 | websiteId: string 5 | } 6 | 7 | const WebsiteInfo = ({websiteId}: Props) => { 8 | return ( 9 |
10 | 11 |
12 | ); 13 | }; 14 | 15 | export default WebsiteInfo; -------------------------------------------------------------------------------- /src/pages/audit/SessionCommandDetail.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Typography} from "antd"; 3 | import {SessionCommand} from "@/src/api/session-api"; 4 | 5 | interface Props { 6 | command?: SessionCommand 7 | } 8 | 9 | const {Title, Paragraph, Text, Link} = Typography; 10 | 11 | const SessionCommandDetail = ({command}: Props) => { 12 | if (!command) { 13 | return
14 | } 15 | return ( 16 |
17 | 18 | Input 19 | 20 |
{command?.command}
21 |
22 | Output 23 | 24 |
{command?.result}
25 |
26 |
27 |
28 | ); 29 | }; 30 | 31 | export default SessionCommandDetail; -------------------------------------------------------------------------------- /src/pages/audit/SessionCommandPage.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {ProCard} from "@ant-design/pro-components"; 3 | import SessionCommandSummary from "@/src/pages/audit/SessionCommandSummary"; 4 | import SessionCommandDetail from "@/src/pages/audit/SessionCommandDetail"; 5 | import {SessionCommand} from "@/src/api/session-api"; 6 | 7 | interface Props { 8 | open: boolean 9 | sessionId: string 10 | } 11 | 12 | const SessionCommandPage = ({open, sessionId}: Props) => { 13 | 14 | const [command, setCommand] = useState(); 15 | return <> 16 | 17 | 18 | setCommand(key)}/> 19 | 20 | 21 | 22 | 23 | 24 | 25 | }; 26 | 27 | export default SessionCommandPage; -------------------------------------------------------------------------------- /src/pages/authorised/CommandFilterDetail.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {Tabs} from "antd"; 3 | import {useParams, useSearchParams} from "react-router-dom"; 4 | import CommandFilterInfo from "./CommandFilterInfo"; 5 | import CommandFilterRulePage from "./CommandFilterRulePage"; 6 | import {maybe} from "@/src/utils/maybe"; 7 | import {useTranslation} from "react-i18next"; 8 | 9 | const CommandFilterDetail = () => { 10 | let params = useParams(); 11 | const id = params['commandFilterId'] as string; 12 | const [searchParams, setSearchParams] = useSearchParams(); 13 | let key = maybe(searchParams.get('activeKey'), 'info'); 14 | let {t} = useTranslation(); 15 | 16 | let [activeKey, setActiveKey] = useState(key); 17 | 18 | const handleTagChange = (key: string) => { 19 | setActiveKey(key); 20 | setSearchParams({'activeKey': key}); 21 | } 22 | 23 | const items = [ 24 | { 25 | label: t('actions.detail'), 26 | key: 'info', 27 | children: 28 | }, 29 | { 30 | label: t('authorised.command_filter.options.rule'), 31 | key: 'rules', 32 | children: 33 | }, 34 | ]; 35 | 36 | return ( 37 |
38 | 39 | 40 | 41 |
42 | ); 43 | }; 44 | 45 | export default CommandFilterDetail; -------------------------------------------------------------------------------- /src/pages/authorised/CommandFilterInfo.tsx: -------------------------------------------------------------------------------- 1 | import commandFilterApi from "../../api/command-filter-api.js"; 2 | import {ProDescriptions} from "@ant-design/pro-components"; 3 | import {useTranslation} from "react-i18next"; 4 | 5 | const api = commandFilterApi; 6 | 7 | interface Props { 8 | id: string 9 | } 10 | 11 | const CommandFilterInfo = ({id}: Props) => { 12 | 13 | let {t} = useTranslation(); 14 | const get = async () => { 15 | let data = await api.getById(id); 16 | return { 17 | success: true, 18 | data: data 19 | } 20 | } 21 | 22 | return ( 23 |
24 | 25 | 26 | 27 | 28 |
29 | ); 30 | }; 31 | 32 | export default CommandFilterInfo; -------------------------------------------------------------------------------- /src/pages/authorised/CommandFilterModal.tsx: -------------------------------------------------------------------------------- 1 | import React, {useRef} from 'react'; 2 | import {Modal} from "antd"; 3 | import commandFilterApi from "../../api/command-filter-api.js"; 4 | import {ProForm, ProFormInstance, ProFormText} from "@ant-design/pro-components"; 5 | import {useTranslation} from "react-i18next"; 6 | 7 | const api = commandFilterApi; 8 | 9 | export interface CommandFilterProps { 10 | open: boolean 11 | handleOk: (values: any) => void 12 | handleCancel: () => void 13 | confirmLoading: boolean 14 | id?: string 15 | } 16 | 17 | const CommandFilterModal = ({ 18 | open, 19 | handleOk, 20 | handleCancel, 21 | confirmLoading, 22 | id 23 | }: CommandFilterProps) => { 24 | 25 | const formRef = useRef(); 26 | let {t} = useTranslation(); 27 | 28 | const get = async () => { 29 | if (id) { 30 | return await api.getById(id); 31 | } 32 | return {}; 33 | } 34 | 35 | return ( 36 | { 42 | formRef.current?.validateFields() 43 | .then(async values => { 44 | handleOk(values); 45 | 46 | }); 47 | }} 48 | onCancel={() => { 49 | 50 | handleCancel(); 51 | }} 52 | confirmLoading={confirmLoading} 53 | > 54 | 55 | 56 | 59 | 60 | ) 61 | }; 62 | 63 | export default CommandFilterModal; 64 | -------------------------------------------------------------------------------- /src/pages/facade/FacadePage.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dushixiang/next-terminal/5a696173f7925992e61f24c5c4088e08f00b24cf/src/pages/facade/FacadePage.css -------------------------------------------------------------------------------- /src/pages/facade/SnippetUserModal.tsx: -------------------------------------------------------------------------------- 1 | import React, {useRef} from 'react'; 2 | import {Modal} from "antd"; 3 | import {ProForm, ProFormInstance, ProFormText, ProFormTextArea} from "@ant-design/pro-components"; 4 | import {useTranslation} from "react-i18next"; 5 | import snippetUserApi from "@/src/api/snippet-user-api"; 6 | 7 | const api = snippetUserApi; 8 | 9 | export interface SnippetProps { 10 | open: boolean 11 | handleOk: (values: any) => void 12 | handleCancel: () => void 13 | confirmLoading: boolean 14 | id: string | undefined 15 | } 16 | 17 | const SnippetUserModal = ({ 18 | open, 19 | handleOk, 20 | handleCancel, 21 | confirmLoading, 22 | id, 23 | }: SnippetProps) => { 24 | let {t} = useTranslation(); 25 | const formRef = useRef(); 26 | 27 | const get = async () => { 28 | if (id) { 29 | return await api.getById(id); 30 | } 31 | return {}; 32 | } 33 | 34 | return ( 35 | 36 | { 42 | formRef.current?.validateFields() 43 | .then(async values => { 44 | handleOk(values); 45 | 46 | }); 47 | }} 48 | onCancel={() => { 49 | 50 | handleCancel(); 51 | }} 52 | confirmLoading={confirmLoading} 53 | > 54 | 55 | 59 | 60 | ) 61 | }; 62 | 63 | export default SnippetUserModal; -------------------------------------------------------------------------------- /src/pages/facade/UserInfoPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import InfoPage from "@/src/pages/account/InfoPage"; 3 | import {useTranslation} from "react-i18next"; 4 | 5 | const UserInfoPage = () => { 6 | let {t} = useTranslation(); 7 | return ( 8 |
9 |
10 |
11 | {t('account.profile')} 12 |
13 |
14 |
15 | 16 |
17 |
18 | ); 19 | }; 20 | 21 | export default UserInfoPage; -------------------------------------------------------------------------------- /src/pages/gateway/AgentGatewayModal.tsx: -------------------------------------------------------------------------------- 1 | import {Modal} from 'antd'; 2 | import React, {useRef} from 'react'; 3 | import {ProForm, ProFormInstance, ProFormText} from "@ant-design/pro-components"; 4 | import {useTranslation} from "react-i18next"; 5 | import agentGatewayApi from "@/src/api/agent-gateway-api"; 6 | 7 | const api = agentGatewayApi; 8 | 9 | interface Props { 10 | open: boolean 11 | handleOk: (values: any) => void 12 | handleCancel: () => void 13 | confirmLoading: boolean 14 | id: string | undefined 15 | } 16 | 17 | const AgentGatewayModal = ({ 18 | open, 19 | handleOk, 20 | handleCancel, 21 | confirmLoading, 22 | id, 23 | }: Props) => { 24 | 25 | const formRef = useRef(); 26 | let {t} = useTranslation(); 27 | 28 | const get = async () => { 29 | if (id) { 30 | return await api.getById(id); 31 | } 32 | return {}; 33 | } 34 | 35 | return ( 36 | { 42 | formRef.current?.validateFields() 43 | .then(async values => { 44 | handleOk(values); 45 | 46 | }); 47 | }} 48 | onCancel={() => { 49 | 50 | handleCancel(); 51 | }} 52 | confirmLoading={confirmLoading} 53 | > 54 | 55 | 56 | 59 | 60 | ); 61 | }; 62 | 63 | export default AgentGatewayModal; -------------------------------------------------------------------------------- /src/pages/gateway/AgentGatewayTokenDrawer.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from 'react'; 2 | import {Drawer, Popconfirm, Table, TableProps, Typography} from "antd"; 3 | import {useTranslation} from "react-i18next"; 4 | import {useQuery} from "@tanstack/react-query"; 5 | import agentGatewayTokenApi, {AgentGatewayToken} from "@/src/api/agent-gateway-token-api"; 6 | import dayjs from "dayjs"; 7 | import NButton from "@/src/components/NButton"; 8 | 9 | interface Props { 10 | open: boolean; 11 | onClose: () => void; 12 | } 13 | 14 | const {Paragraph} = Typography; 15 | 16 | const AgentGatewayTokenDrawer = ({open, onClose}: Props) => { 17 | 18 | let {t} = useTranslation(); 19 | 20 | let tokenQuery = useQuery({ 21 | queryKey: ['agent-gateway-tokens'], 22 | queryFn: agentGatewayTokenApi.getAll, 23 | }); 24 | 25 | useEffect(() => { 26 | if (open) { 27 | tokenQuery.refetch(); 28 | } 29 | }, [open]); 30 | 31 | const columns: TableProps['columns'] = [ 32 | { 33 | title: 'Token', 34 | dataIndex: 'id', 35 | key: 'id', 36 | render: (text) => { 37 | return 38 | {text} 39 | 40 | } 41 | }, 42 | { 43 | title: t('general.updated_at'), 44 | dataIndex: 'updatedAt', 45 | key: 'updatedAt', 46 | width: 191, 47 | render: (text) => { 48 | return dayjs(text).format('YYYY-MM-DD HH:mm:ss') 49 | } 50 | }, 51 | { 52 | title: t('actions.option'), 53 | key: 'action', 54 | render: (_, record) => ( 55 | { 59 | await agentGatewayTokenApi.deleteById(record.id); 60 | tokenQuery.refetch(); 61 | }} 62 | > 63 | {t('actions.delete')} 64 | 65 | ), 66 | }, 67 | ]; 68 | 69 | return ( 70 | 75 |
76 | 77 | columns={columns} 78 | dataSource={tokenQuery.data} 79 | pagination={false} 80 | /> 81 |
82 |
83 | ); 84 | }; 85 | 86 | export default AgentGatewayTokenDrawer; -------------------------------------------------------------------------------- /src/pages/identity/GroupDetail.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {useParams, useSearchParams} from "react-router-dom"; 3 | import {Tabs} from "antd"; 4 | import GroupInfo from "./GroupInfo"; 5 | import {maybe} from "../../utils/maybe"; 6 | import UserAuthorised from "@/src/pages/authorised/UserAuthorised"; 7 | import {useTranslation} from "react-i18next"; 8 | 9 | const GroupDetail = () => { 10 | let params = useParams(); 11 | const id = params['userGroupId'] as string; 12 | const [searchParams, setSearchParams] = useSearchParams(); 13 | let key = maybe(searchParams.get('activeKey'), 'detail'); 14 | 15 | let [activeKey, setActiveKey] = useState(key); 16 | 17 | let {t} = useTranslation(); 18 | 19 | const handleTagChange = (key: string) => { 20 | setActiveKey(key); 21 | setSearchParams({'activeKey': key}); 22 | } 23 | 24 | const items = [ 25 | { 26 | label: t('actions.detail'), 27 | key: 'detail', 28 | children: 29 | }, 30 | { 31 | label: t('identity.options.authorized_asset'), 32 | key: 'asset', 33 | children: 34 | } 35 | ]; 36 | 37 | return ( 38 |
39 | 40 | 41 | 42 |
43 | ); 44 | }; 45 | 46 | export default GroupDetail; -------------------------------------------------------------------------------- /src/pages/identity/GroupInfo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import userGroupApi from "../../api/user-group-api"; 3 | import {ProDescriptions} from "@ant-design/pro-components"; 4 | import {useTranslation} from "react-i18next"; 5 | 6 | const api = userGroupApi; 7 | 8 | interface UserGroupInfoProps { 9 | active: boolean 10 | id: string 11 | } 12 | 13 | const GroupInfo = ({active, id}: UserGroupInfoProps) => { 14 | 15 | let {t} = useTranslation(); 16 | 17 | const get = async () => { 18 | let data = await api.getById(id); 19 | return { 20 | success: true, 21 | data: data 22 | } 23 | } 24 | 25 | return ( 26 |
27 | 28 | 29 | 30 | 31 |
32 | ); 33 | }; 34 | 35 | export default GroupInfo; -------------------------------------------------------------------------------- /src/pages/identity/GroupModal.tsx: -------------------------------------------------------------------------------- 1 | import {Modal} from "antd"; 2 | import userGroupApi, {UserGroup} from "../../api/user-group-api"; 3 | import userApi from "../../api/user-api"; 4 | import {ProForm, ProFormInstance, ProFormSelect, ProFormText} from "@ant-design/pro-components"; 5 | import {useRef} from "react"; 6 | import {useTranslation} from "react-i18next"; 7 | 8 | const api = userGroupApi; 9 | 10 | export interface GroupModalProps { 11 | open: boolean 12 | handleOk: (values: any) => void 13 | handleCancel: () => void 14 | confirmLoading: boolean 15 | id: string | undefined 16 | } 17 | 18 | const GroupModal = ({ 19 | open, 20 | handleOk, 21 | handleCancel, 22 | confirmLoading, 23 | id, 24 | }: GroupModalProps) => { 25 | 26 | let {t} = useTranslation(); 27 | const formRef = useRef(); 28 | 29 | const get = async () => { 30 | if (id) { 31 | let data = await api.getById(id); 32 | if (!data.members) { 33 | data.members = []; 34 | } 35 | return data; 36 | } 37 | return {} as UserGroup; 38 | } 39 | 40 | return ( 41 | { 47 | formRef.current?.validateFields() 48 | .then(async values => { 49 | handleOk(values); 50 | }); 51 | }} 52 | onCancel={() => { 53 | handleCancel(); 54 | }} 55 | confirmLoading={confirmLoading} 56 | > 57 | 58 | 59 | 75 | 76 | ) 77 | }; 78 | 79 | export default GroupModal; 80 | -------------------------------------------------------------------------------- /src/pages/identity/LoginPolicyDetailPage.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {Tabs} from "antd"; 3 | import {useParams, useSearchParams} from "react-router-dom"; 4 | import LoginPolicyInfo from "./LoginPolicyInfo"; 5 | import LoginPolicyUser from "./LoginPolicyUser"; 6 | import {useTranslation} from "react-i18next"; 7 | 8 | const LoginPolicyDetailPage = () => { 9 | let {t} = useTranslation(); 10 | let params = useParams(); 11 | const loginPolicyId = params['loginPolicyId']; 12 | const [searchParams, setSearchParams] = useSearchParams(); 13 | let key = searchParams.get('activeKey'); 14 | key = key ? key : 'detail'; 15 | 16 | let [activeKey, setActiveKey] = useState(key); 17 | 18 | const handleTagChange = (key: string) => { 19 | setActiveKey(key); 20 | setSearchParams({'activeKey': key}); 21 | } 22 | 23 | const items = [ 24 | { 25 | label: t('actions.detail'), 26 | key: 'detail', 27 | children: 28 | }, 29 | { 30 | label: t('actions.binding'), 31 | key: 'bind-user', 32 | children: 33 | }, 34 | // { 35 | // label: '绑定用户组', 36 | // key: 'bind-user-group', 37 | // disabled: !hasMenu('login-policy-bind-user-group'), 38 | // children: 39 | // } 40 | ]; 41 | 42 | return ( 43 |
44 | 45 | 46 | 47 |
48 | ); 49 | }; 50 | 51 | export default LoginPolicyDetailPage; -------------------------------------------------------------------------------- /src/pages/identity/LoginPolicyUser.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import {Transfer} from "antd"; 3 | import {TransferDirection} from "antd/es/transfer"; 4 | import {useQuery} from "@tanstack/react-query"; 5 | import userApi from "../../api/user-api"; 6 | import loginPolicyApi from "../../api/login-policy-api"; 7 | import {useTranslation} from "react-i18next"; 8 | 9 | 10 | const LoginPolicyUser = ({id}: any) => { 11 | let {t} = useTranslation(); 12 | const [targetKeys, setTargetKeys] = useState([]); 13 | 14 | let usersQuery = useQuery({ 15 | queryKey: ['user'], 16 | queryFn: userApi.getAll, 17 | }); 18 | 19 | let selectedKeysQuery = useQuery({ 20 | queryKey: ['user-id'], 21 | queryFn: () => loginPolicyApi.getUserId(id) 22 | }); 23 | 24 | useEffect(() => { 25 | if (selectedKeysQuery.data) { 26 | setTargetKeys(selectedKeysQuery.data) 27 | } 28 | }, [selectedKeysQuery.data]); 29 | 30 | if (usersQuery.isLoading) { 31 | return
Loading...
32 | } 33 | 34 | let items = usersQuery.data?.map(item => { 35 | return { 36 | key: item.id, 37 | title: item.nickname, 38 | } 39 | }); 40 | 41 | const onChange = async (nextTargetKeys: string[], direction: TransferDirection, moveKeys: string[]) => { 42 | switch (direction){ 43 | case 'left': 44 | await loginPolicyApi.unbindUser(id, moveKeys); 45 | break; 46 | case 'right': 47 | await loginPolicyApi.bindUser(id, moveKeys); 48 | break; 49 | } 50 | setTargetKeys(nextTargetKeys); 51 | }; 52 | 53 | return ( 54 | item.title} 66 | /> 67 | ); 68 | }; 69 | 70 | export default LoginPolicyUser; -------------------------------------------------------------------------------- /src/pages/identity/RoleDetail.tsx: -------------------------------------------------------------------------------- 1 | import {useParams, useSearchParams} from "react-router-dom"; 2 | import {Tabs} from "antd"; 3 | import RoleInfo from "./RoleInfo"; 4 | import React, {useState} from "react"; 5 | import {maybe} from "../../utils/maybe"; 6 | import {useTranslation} from "react-i18next"; 7 | 8 | const RoleDetail = () => { 9 | 10 | let {t} = useTranslation(); 11 | let params = useParams(); 12 | const id = params['roleId'] as string; 13 | const [searchParams, setSearchParams] = useSearchParams(); 14 | let key = maybe(searchParams.get('activeKey'), 'detail'); 15 | 16 | let [activeKey, setActiveKey] = useState(key); 17 | 18 | const handleTagChange = (key: string) => { 19 | setActiveKey(key); 20 | setSearchParams({'activeKey': key}); 21 | } 22 | 23 | const items = [ 24 | { 25 | label: t('actions.detail'), 26 | key: 'detail', 27 | children: 28 | }, 29 | ]; 30 | 31 | return ( 32 |
33 | 34 | 35 | 36 |
37 | ); 38 | } 39 | 40 | export default RoleDetail; -------------------------------------------------------------------------------- /src/pages/identity/UserDetailPage.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {Tabs} from "antd"; 3 | import UserInfo from "./UserInfo"; 4 | import UserLoginPolicy from "./UserLoginPolicy"; 5 | import {useParams, useSearchParams} from "react-router-dom"; 6 | import {maybe} from "../../utils/maybe"; 7 | import UserAuthorised from "@/src/pages/authorised/UserAuthorised"; 8 | import {useTranslation} from "react-i18next"; 9 | 10 | const UserDetailPage = () => { 11 | 12 | let {t} = useTranslation(); 13 | let params = useParams(); 14 | const id = params['userId'] as string; 15 | const [searchParams, setSearchParams] = useSearchParams(); 16 | let key = maybe(searchParams.get('activeKey'), 'info'); 17 | 18 | let [activeKey, setActiveKey] = useState(key); 19 | 20 | const handleTagChange = (key: string) => { 21 | setActiveKey(key); 22 | setSearchParams({'activeKey': key}); 23 | } 24 | 25 | const items = [ 26 | { 27 | label: t('actions.detail'), 28 | key: 'info', 29 | children: 30 | }, 31 | { 32 | label: t('identity.options.authorized_asset'), 33 | key: 'asset', 34 | children: 35 | }, 36 | { 37 | label: t('identity.options.login_policy'), 38 | key: 'login-policy', 39 | children: 40 | }, 41 | ]; 42 | 43 | return ( 44 |
45 | 46 | 47 |
48 | ); 49 | } 50 | 51 | export default UserDetailPage; -------------------------------------------------------------------------------- /src/pages/identity/UserInfo.tsx: -------------------------------------------------------------------------------- 1 | import userApi from "../../api/user-api"; 2 | import {ProDescriptions} from "@ant-design/pro-components"; 3 | import React from "react"; 4 | import {useTranslation} from "react-i18next"; 5 | 6 | interface UserInfoProps { 7 | active: boolean 8 | id: string 9 | } 10 | 11 | const UserInfo = ({active, id}: UserInfoProps) => { 12 | 13 | let {t} = useTranslation(); 14 | 15 | const get = async () => { 16 | let data = await userApi.getById(id); 17 | return { 18 | success: true, 19 | data: data 20 | } 21 | } 22 | 23 | return ( 24 |
25 | 26 | 27 | 28 | 29 | 39 | 49 | 50 | 51 |
52 | ); 53 | }; 54 | 55 | export default UserInfo; -------------------------------------------------------------------------------- /src/pages/identity/UserLoginPolicy.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from "react"; 2 | import {useQuery} from "@tanstack/react-query"; 3 | import loginPolicyApi from "../../api/login-policy-api"; 4 | import {TransferDirection} from "antd/es/transfer"; 5 | import {Transfer} from "antd"; 6 | import {useTranslation} from "react-i18next"; 7 | 8 | interface UserInfoProps { 9 | active: boolean 10 | userId: string 11 | } 12 | 13 | const UserLoginPolicy = ({active, userId}: UserInfoProps) => { 14 | let {t} = useTranslation(); 15 | const [targetKeys, setTargetKeys] = useState([]); 16 | 17 | let loginPolicyQuery = useQuery({ 18 | queryKey: ['loginPolicy'], 19 | queryFn: loginPolicyApi.getAll, 20 | }); 21 | 22 | let selectedKeysQuery = useQuery({ 23 | queryKey: ['login-policy-id'], 24 | queryFn: () => loginPolicyApi.getLoginPolicyIdByUserId(userId) 25 | }); 26 | 27 | useEffect(() => { 28 | if (selectedKeysQuery.data) { 29 | setTargetKeys(selectedKeysQuery.data) 30 | } 31 | }, [selectedKeysQuery.data]); 32 | 33 | if (loginPolicyQuery.isLoading) { 34 | return
Loading...
35 | } 36 | 37 | let items = loginPolicyQuery.data?.map(item => { 38 | return { 39 | key: item.id, 40 | title: item.name, 41 | priority: item.priority, 42 | } 43 | }); 44 | 45 | const onChange = async (nextTargetKeys: string[], direction: TransferDirection, moveKeys: string[]) => { 46 | switch (direction) { 47 | case 'left': 48 | await loginPolicyApi.unbindLoginPolicy(userId, moveKeys); 49 | break; 50 | case 'right': 51 | await loginPolicyApi.bindLoginPolicy(userId, moveKeys); 52 | break; 53 | } 54 | setTargetKeys(nextTargetKeys); 55 | }; 56 | 57 | return ( 58 | item.title + `(${item.priority})`} 70 | /> 71 | ); 72 | }; 73 | 74 | export default UserLoginPolicy; -------------------------------------------------------------------------------- /src/pages/identity/UserResetPasswordModal.tsx: -------------------------------------------------------------------------------- 1 | import React, {useRef} from 'react'; 2 | import {ProForm, ProFormInstance, ProFormText} from "@ant-design/pro-components"; 3 | import {Modal} from "antd"; 4 | import {useTranslation} from "react-i18next"; 5 | import {WarningTwoTone} from "@ant-design/icons"; 6 | 7 | export interface Props { 8 | open: boolean 9 | handleOk: (values: any) => void 10 | handleCancel: () => void 11 | confirmLoading: boolean 12 | } 13 | 14 | const UserResetPasswordModal = ({open, handleOk, handleCancel, confirmLoading}: Props) => { 15 | 16 | const formRef = useRef(); 17 | let {t} = useTranslation(); 18 | 19 | return ( 20 | { 26 | formRef.current?.validateFields() 27 | .then(async values => { 28 | handleOk(values); 29 | 30 | }); 31 | }} 32 | onCancel={() => { 33 | 34 | handleCancel(); 35 | }} 36 | confirmLoading={confirmLoading} 37 | > 38 | 39 | 40 | 50 | 51 | 52 | ); 53 | }; 54 | 55 | export default UserResetPasswordModal; -------------------------------------------------------------------------------- /src/pages/sysconf/BackupSetting.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {Alert, Button, message, Space, Typography} from "antd"; 3 | import requests from "@/src/api/core/requests"; 4 | import {useTranslation} from "react-i18next"; 5 | 6 | const {Title} = Typography; 7 | 8 | const BackupSetting = () => { 9 | 10 | let {t} = useTranslation(); 11 | let [loading, setLoading] = useState(false); 12 | 13 | const handleImport = (files: FileList | null) => { 14 | if (!files) { 15 | return; 16 | } 17 | const reader = new FileReader(); 18 | reader.onload = async () => { 19 | let backup = JSON.parse(reader.result.toString()); 20 | setLoading(true); 21 | try { 22 | await requests.post('/admin/backup/import', backup); 23 | message.success('恢复成功', 3); 24 | } finally { 25 | // window.document.getElementById('file-upload').value = ""; 26 | setLoading(false); 27 | } 28 | }; 29 | reader.readAsText(files[0]); 30 | } 31 | 32 | return ( 33 |
34 | {t('settings.backup.setting')} 35 | 36 | 37 | 41 | 42 | 43 | {/**/} 48 | 49 | 54 | { 59 | let files = e.target.files; 60 | await handleImport(files); 61 | e.target.value = ''; 62 | }} 63 | /> 64 | 65 | 66 |
67 | ); 68 | }; 69 | 70 | export default BackupSetting; -------------------------------------------------------------------------------- /src/pages/sysconf/IdentitySetting.tsx: -------------------------------------------------------------------------------- 1 | import {Tabs, TabsProps} from 'antd'; 2 | import React from 'react'; 3 | import {SettingProps} from "@/src/pages/sysconf/SettingPage"; 4 | import LdapSetting from "@/src/pages/sysconf/LdapSetting"; 5 | import {useTranslation} from "react-i18next"; 6 | import WebAuthnSetting from "@/src/pages/sysconf/WebAuthnSetting"; 7 | 8 | const IdentitySetting = ({get, set}: SettingProps) => { 9 | 10 | let {t} = useTranslation(); 11 | 12 | const items: TabsProps['items'] = [ 13 | { 14 | key: 'webauthn', 15 | label: t('settings.webauthn.setting'), 16 | children: , 17 | }, 18 | { 19 | key: 'ldap', 20 | label: t('settings.ldap.setting'), 21 | children: , 22 | }, 23 | ]; 24 | 25 | return ( 26 |
27 | 28 |
29 | ); 30 | }; 31 | 32 | export default IdentitySetting; -------------------------------------------------------------------------------- /src/pages/sysops/ScheduledTaskRuntime.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from 'react'; 2 | import {useQuery} from "@tanstack/react-query"; 3 | import scheduledTaskApi from '@/src/api/scheduled-task-api'; 4 | 5 | interface Props { 6 | open: boolean 7 | spec: string; 8 | } 9 | 10 | const ScheduledTaskRuntime = ({open, spec}: Props) => { 11 | 12 | let query = useQuery({ 13 | queryKey: ['scheduled-task-runtime', spec], 14 | queryFn: () => { 15 | return scheduledTaskApi.getNextTenRuns(spec); 16 | }, 17 | enabled: open, 18 | retry: false, 19 | }); 20 | 21 | useEffect(() => { 22 | if (open) { 23 | query.refetch(); 24 | } 25 | }, [open]); 26 | 27 | return ( 28 |
29 | {query.isError &&
Error: {query.error?.message}
} 30 |
31 | {query.data?.map((item: any) => { 32 | return
{item}
33 | })} 34 |
35 |
36 | ); 37 | }; 38 | 39 | export default ScheduledTaskRuntime; -------------------------------------------------------------------------------- /src/pages/sysops/SystemMonitorPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | 4 | const SystemMonitorPage = () => { 5 | return ( 6 |
7 |
8 | 尚未完成... 9 |
10 |
11 | ); 12 | }; 13 | 14 | export default SystemMonitorPage; -------------------------------------------------------------------------------- /src/pages/sysops/ToolsPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Tabs, TabsProps} from "antd"; 3 | import {useSearchParams} from "react-router-dom"; 4 | import ToolsPing from './ToolsPing'; 5 | import ToolsTcping from "@/src/pages/sysops/ToolsTcping"; 6 | import ToolsTraceRoute from "@/src/pages/sysops/ToolsTraceRoute"; 7 | 8 | const ToolsPage = () => { 9 | 10 | let [searchParams, setSearchParams] = useSearchParams(); 11 | 12 | const items: TabsProps['items'] = [ 13 | { 14 | key: 'ping', 15 | label: 'Ping', 16 | children: , 17 | }, 18 | { 19 | key: 'tcping', 20 | label: 'TCP Ping', 21 | children: , 22 | }, 23 | { 24 | key: 'traceroute', 25 | label: 'Trace Route', 26 | children: , 27 | }, 28 | ]; 29 | 30 | const onChange = (key: string) => { 31 | searchParams.set('tab', key); 32 | setSearchParams(searchParams); 33 | } 34 | 35 | return ( 36 |
37 | 41 |
42 | ); 43 | }; 44 | 45 | export default ToolsPage; -------------------------------------------------------------------------------- /src/pages/sysops/ToolsTraceRoute.tsx: -------------------------------------------------------------------------------- 1 | import {Button, Form, Input} from 'antd'; 2 | import React, {useState} from 'react'; 3 | import {baseUrl, getToken} from "@/src/api/core/requests"; 4 | import {useTranslation} from "react-i18next"; 5 | 6 | const ToolsTraceRoute = () => { 7 | 8 | let {t} = useTranslation(); 9 | const [host, setHost] = useState(""); 10 | const [logs, setLogs] = useState([]); 11 | const [running, setRunning] = useState(false); 12 | let eventSource: EventSource | null = null; 13 | 14 | const onSearch = (host: string) => { 15 | if (running) return; // 防止重复启动 16 | setRunning(true); 17 | setLogs([]); 18 | 19 | eventSource = new EventSource(`${baseUrl()}/admin/tools/traceroute?X-Auth-Token=${getToken()}&host=${host}`); 20 | 21 | eventSource.onmessage = (event) => { 22 | setLogs((prevLogs) => [...prevLogs, event.data]); 23 | }; 24 | 25 | eventSource.onerror = () => { 26 | // setLogs((prevLogs) => [...prevLogs, "Connection closed."]); 27 | eventSource?.close(); 28 | setRunning(false); 29 | }; 30 | } 31 | 32 | return ( 33 |
34 |
35 | 38 | setHost(e.target.value)} 41 | placeholder={t('sysops.tools.target_placeholder')} 42 | style={{ 43 | width: '200px' 44 | }} 45 | /> 46 | 47 | 48 | 55 | 56 |
57 | 58 |
59 |
{logs.join("\n")}
60 |
61 |
62 | ); 63 | }; 64 | 65 | export default ToolsTraceRoute; -------------------------------------------------------------------------------- /src/react-i18next/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import {initReactI18next} from 'react-i18next'; 3 | import {resources} from "./locales/resources"; 4 | 5 | let savedLang = localStorage.getItem('nt-language'); 6 | if (savedLang) { 7 | savedLang = savedLang.replaceAll(`"`, ""); 8 | } 9 | console.log(`get lang: ${savedLang}`); 10 | 11 | i18n 12 | // 将 i18n 实例传递给 react-i18next 13 | .use(initReactI18next) 14 | // 初始化 i18next 15 | // 所有配置选项: https://www.i18next.com/overview/configuration-options 16 | .init({ 17 | resources, 18 | fallbackLng: "en-US", 19 | lng: savedLang, 20 | debug: true, 21 | interpolation: { 22 | escapeValue: false // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape 23 | } 24 | }); 25 | 26 | export default i18n; -------------------------------------------------------------------------------- /src/react-i18next/locales/code2json.ts: -------------------------------------------------------------------------------- 1 | // 将对象转换为 JSON 字符串 2 | import zh_cn from "./zh-CN"; 3 | import * as fs from "node:fs"; 4 | 5 | const jsonString = JSON.stringify(zh_cn, null, 2); // null, 2 是为了格式化输出 6 | 7 | // 将 JSON 字符串写入到本地文件 8 | fs.writeFile('zh-CN.json', jsonString, 'utf8', (err) => { 9 | if (err) { 10 | console.error('An error occurred while writing JSON Object to File.', err); 11 | return; 12 | } 13 | console.log('JSON file has been saved.'); 14 | }); -------------------------------------------------------------------------------- /src/react-i18next/locales/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "llm": { 3 | "provider": "custom", 4 | "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", 5 | "model": "qwen2.5-72b-instruct" 6 | }, 7 | "input": "zh-CN.json", 8 | "from": "zh-CN", 9 | "to": [ 10 | "zh-TW", 11 | "en-US", 12 | "ja-JP" 13 | ] 14 | } -------------------------------------------------------------------------------- /src/react-i18next/locales/resources.ts: -------------------------------------------------------------------------------- 1 | import zhCN from "./zh-CN"; 2 | import enUS from "./en-US.json"; 3 | import zhTW from "./zh-TW.json"; 4 | import jaJP from "./ja-JP.json"; 5 | 6 | export const resources = { 7 | "en-US": { 8 | translation: enUS 9 | }, 10 | "zh-CN": { 11 | translation: zhCN 12 | }, 13 | "zh-TW": { 14 | translation: zhTW 15 | }, 16 | "ja-JP": { 17 | translation: jaJP 18 | } 19 | } -------------------------------------------------------------------------------- /src/react-i18next/locales/translates.sh: -------------------------------------------------------------------------------- 1 | deno run --allow-write --unstable-sloppy-imports code2json.ts 2 | jsont -c config.json -k "${DASHSCOPE_API_KEY}" -------------------------------------------------------------------------------- /src/utils/array.ts: -------------------------------------------------------------------------------- 1 | class Arrays { 2 | isEmpty = function (array: any[]) { 3 | if (array) { 4 | return array.length === 0; 5 | } 6 | return true; 7 | } 8 | } 9 | 10 | let arrays = new Arrays(); 11 | export default arrays; -------------------------------------------------------------------------------- /src/utils/codec.ts: -------------------------------------------------------------------------------- 1 | import {Base64} from "js-base64"; 2 | 3 | export const safeEncode = (data: any) => { 4 | let s = JSON.stringify(data); 5 | return Base64.encode(s, true); 6 | } 7 | 8 | export const safeDecode = (data: string) => { 9 | let s = Base64.decode(data); 10 | return JSON.parse(s); 11 | } -------------------------------------------------------------------------------- /src/utils/debounce.ts: -------------------------------------------------------------------------------- 1 | export function debounce(cb: T, wait = 20) { 2 | let h: number; 3 | let callable = (...args: any) => { 4 | clearTimeout(h); 5 | // @ts-ignore 6 | h = setTimeout(() => cb(...args), wait); 7 | }; 8 | return (callable); 9 | } -------------------------------------------------------------------------------- /src/utils/duration.ts: -------------------------------------------------------------------------------- 1 | export class Duration { 2 | private readonly milliseconds: number; 3 | 4 | constructor(milliseconds: number) { 5 | this.milliseconds = milliseconds; 6 | } 7 | 8 | static fromObject({ minutes = 0, seconds = 0, milliseconds = 0 }: { minutes?: number, seconds?: number, milliseconds?: number }): Duration { 9 | const totalMilliseconds = minutes * 60 * 1000 + seconds * 1000 + milliseconds; 10 | return new Duration(totalMilliseconds); 11 | } 12 | 13 | format(): string { 14 | const minutes = Math.floor(this.milliseconds / (60 * 1000)); 15 | const seconds = Math.floor((this.milliseconds % (60 * 1000)) / 1000); 16 | // const milliseconds = this.milliseconds % 1000; 17 | 18 | let formattedString = ""; 19 | if (minutes > 0) { 20 | formattedString += `${minutes}m`; 21 | } 22 | if (seconds > 0) { 23 | formattedString += `${seconds}s`; 24 | } 25 | // if (milliseconds > 0) { 26 | // formattedString += `.${milliseconds}`; 27 | // } 28 | 29 | return formattedString; 30 | } 31 | } -------------------------------------------------------------------------------- /src/utils/global.ts: -------------------------------------------------------------------------------- 1 | import {AccountInfo} from "@/src/api/account-api"; 2 | import {Branding} from "@/src/api/branding-api"; 3 | 4 | export type Global = { 5 | user: AccountInfo; 6 | branding?: Branding 7 | } 8 | 9 | export const global = window as any as Global; -------------------------------------------------------------------------------- /src/utils/maybe.ts: -------------------------------------------------------------------------------- 1 | export function maybe(v: any | undefined | null, r: R): R { 2 | if (v) { 3 | return v; 4 | } 5 | return r 6 | } -------------------------------------------------------------------------------- /src/utils/permission.ts: -------------------------------------------------------------------------------- 1 | import {global} from './global' 2 | import {AccountInfo} from "@/src/api/account-api"; 3 | 4 | export function isAdmin() { 5 | let user = getCurrentUser(); 6 | return user['type'] === 'admin'; 7 | } 8 | 9 | export function clearCurrentUser() { 10 | global.user = null; 11 | } 12 | 13 | export function setCurrentUser(user: AccountInfo) { 14 | global.user = user 15 | } 16 | 17 | export function getCurrentUser() { 18 | return global.user; 19 | } 20 | 21 | export function hasMenu(...items: string[]) { 22 | // return true 23 | let menus = getCurrentUser()?.menus || []; 24 | let filtered = menus.map(item => item.key); 25 | for (const item of items) { 26 | if (filtered.includes(item)) { 27 | return true; 28 | } 29 | } 30 | return false; 31 | } -------------------------------------------------------------------------------- /src/utils/strings.ts: -------------------------------------------------------------------------------- 1 | class Strings { 2 | hasText = function (text: string | undefined | null) { 3 | return !(text === undefined || text === null || text.length === 0); 4 | } 5 | 6 | zeroPad = function zeroPad(num: number, minLength: number) { 7 | let str = num.toString(); 8 | while (str.length < minLength) 9 | str = '0' + str; 10 | return str; 11 | }; 12 | 13 | isTrue = function (text: string) { 14 | return (/^true$/i).test(text); 15 | } 16 | } 17 | 18 | let strings = new Strings(); 19 | export default strings; -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: [ 5 | './pages/**/*.{ts,tsx}', 6 | './components/**/*.{ts,tsx}', 7 | './app/**/*.{ts,tsx}', 8 | './src/**/*.{ts,tsx}', 9 | ], 10 | prefix: "", 11 | theme: { 12 | container: { 13 | center: true, 14 | padding: "2rem", 15 | screens: { 16 | "2xl": "1400px", 17 | }, 18 | }, 19 | extend: { 20 | colors: { 21 | border: "hsl(var(--border))", 22 | input: "hsl(var(--input))", 23 | ring: "hsl(var(--ring))", 24 | background: "hsl(var(--background))", 25 | foreground: "hsl(var(--foreground))", 26 | primary: { 27 | DEFAULT: "hsl(var(--primary))", 28 | foreground: "hsl(var(--primary-foreground))", 29 | }, 30 | secondary: { 31 | DEFAULT: "hsl(var(--secondary))", 32 | foreground: "hsl(var(--secondary-foreground))", 33 | }, 34 | destructive: { 35 | DEFAULT: "hsl(var(--destructive))", 36 | foreground: "hsl(var(--destructive-foreground))", 37 | }, 38 | muted: { 39 | DEFAULT: "hsl(var(--muted))", 40 | foreground: "hsl(var(--muted-foreground))", 41 | }, 42 | accent: { 43 | DEFAULT: "hsl(var(--accent))", 44 | foreground: "hsl(var(--accent-foreground))", 45 | }, 46 | popover: { 47 | DEFAULT: "hsl(var(--popover))", 48 | foreground: "hsl(var(--popover-foreground))", 49 | }, 50 | card: { 51 | DEFAULT: "hsl(var(--card))", 52 | foreground: "hsl(var(--card-foreground))", 53 | }, 54 | }, 55 | borderRadius: { 56 | lg: "var(--radius)", 57 | md: "calc(var(--radius) - 2px)", 58 | sm: "calc(var(--radius) - 4px)", 59 | }, 60 | keyframes: { 61 | "accordion-down": { 62 | from: { height: "0" }, 63 | to: { height: "var(--radix-accordion-content-height)" }, 64 | }, 65 | "accordion-up": { 66 | from: { height: "var(--radix-accordion-content-height)" }, 67 | to: { height: "0" }, 68 | }, 69 | "caret-blink": { 70 | "0%,70%,100%": { opacity: "1" }, 71 | "20%,50%": { opacity: "0" }, 72 | }, 73 | }, 74 | animation: { 75 | "accordion-down": "accordion-down 0.2s ease-out", 76 | "accordion-up": "accordion-up 0.2s ease-out", 77 | "caret-blink": "caret-blink 1.25s ease-out infinite", 78 | }, 79 | }, 80 | }, 81 | plugins: [require("tailwindcss-animate")], 82 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": false, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "baseUrl": ".", 19 | "paths": { 20 | "@/*": ["./*"] 21 | } 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import {resolve} from 'path'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | resolve: { 9 | alias: { '@': resolve(__dirname, './') }, 10 | }, 11 | }) 12 | --------------------------------------------------------------------------------