├── .air.toml ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── assets │ ├── 1ms.svg │ ├── ddunyun.png │ ├── dk.png │ ├── ui.png │ ├── wafpro.png │ └── wxd.png └── workflows │ ├── build.yml │ ├── goreleaser.yml │ ├── issue-auto-close.yml │ ├── issue-auto-reply.yml │ ├── l10n.yml │ ├── lint.yml │ ├── mockery.yml │ └── test.yml ├── .gitignore ├── .goreleaser.yaml ├── .mockery.yaml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── README_EN.md ├── SECURITY.md ├── cmd ├── README.md ├── cli │ ├── main.go │ ├── wire.go │ └── wire_gen.go └── web │ ├── main.go │ ├── wire.go │ └── wire_gen.go ├── config.example.yml ├── crowdin.yml ├── go.mod ├── go.sum ├── internal ├── app │ ├── cli.go │ ├── global.go │ └── web.go ├── apps │ ├── apps.go │ ├── codeserver │ │ ├── app.go │ │ └── request.go │ ├── docker │ │ ├── app.go │ │ └── request.go │ ├── fail2ban │ │ ├── app.go │ │ ├── request.go │ │ └── types.go │ ├── frp │ │ ├── app.go │ │ └── request.go │ ├── gitea │ │ ├── app.go │ │ └── request.go │ ├── memcached │ │ ├── app.go │ │ └── request.go │ ├── minio │ │ ├── app.go │ │ └── request.go │ ├── mysql │ │ ├── app.go │ │ └── request.go │ ├── nginx │ │ ├── app.go │ │ └── request.go │ ├── php │ │ ├── app.go │ │ ├── request.go │ │ └── types.go │ ├── php74 │ │ └── app.go │ ├── php80 │ │ └── app.go │ ├── php81 │ │ └── app.go │ ├── php82 │ │ └── app.go │ ├── php83 │ │ └── app.go │ ├── php84 │ │ └── app.go │ ├── phpmyadmin │ │ ├── app.go │ │ └── request.go │ ├── podman │ │ ├── app.go │ │ └── request.go │ ├── postgresql │ │ ├── app.go │ │ └── request.go │ ├── pureftpd │ │ ├── app.go │ │ ├── request.go │ │ └── types.go │ ├── redis │ │ ├── app.go │ │ └── request.go │ ├── rsync │ │ ├── app.go │ │ ├── request.go │ │ └── types.go │ ├── s3fs │ │ ├── app.go │ │ ├── request.go │ │ └── types.go │ └── supervisor │ │ ├── app.go │ │ ├── request.go │ │ └── types.go ├── biz │ ├── app.go │ ├── backup.go │ ├── cache.go │ ├── cert.go │ ├── cert_account.go │ ├── cert_dns.go │ ├── container.go │ ├── container_compose.go │ ├── container_image.go │ ├── container_network.go │ ├── container_volume.go │ ├── cron.go │ ├── database.go │ ├── database_server.go │ ├── database_user.go │ ├── monitor.go │ ├── safe.go │ ├── setting.go │ ├── ssh.go │ ├── task.go │ ├── user.go │ ├── user_token.go │ └── website.go ├── bootstrap │ ├── apps.go │ ├── bootstrap.go │ ├── cli.go │ ├── conf.go │ ├── cron.go │ ├── db.go │ ├── http.go │ ├── logger.go │ ├── queue.go │ ├── session.go │ ├── t.go │ └── validator.go ├── data │ ├── app.go │ ├── backup.go │ ├── cache.go │ ├── cert.go │ ├── cert_account.go │ ├── cert_dns.go │ ├── container.go │ ├── container_compose.go │ ├── container_image.go │ ├── container_network.go │ ├── container_volume.go │ ├── cron.go │ ├── data.go │ ├── database.go │ ├── database_server.go │ ├── database_user.go │ ├── helper.go │ ├── monitor.go │ ├── safe.go │ ├── setting.go │ ├── ssh.go │ ├── task.go │ ├── user.go │ ├── user_token.go │ └── website.go ├── http │ ├── middleware │ │ ├── entrance.go │ │ ├── helper.go │ │ ├── middleware.go │ │ ├── must_install.go │ │ ├── must_login.go │ │ ├── status.go │ │ └── throttle.go │ ├── request │ │ ├── app.go │ │ ├── backup.go │ │ ├── cert.go │ │ ├── cert_account.go │ │ ├── cert_dns.go │ │ ├── common.go │ │ ├── container.go │ │ ├── container_compose.go │ │ ├── container_image.go │ │ ├── container_network.go │ │ ├── container_volume.go │ │ ├── cron.go │ │ ├── dashboard.go │ │ ├── database.go │ │ ├── database_server.go │ │ ├── database_user.go │ │ ├── file.go │ │ ├── firewall.go │ │ ├── monitor.go │ │ ├── paginate.go │ │ ├── process.go │ │ ├── request.go │ │ ├── safe.go │ │ ├── setting.go │ │ ├── ssh.go │ │ ├── systemctl.go │ │ ├── toolbox_benchmark.go │ │ ├── toolbox_system.go │ │ ├── user.go │ │ ├── user_token.go │ │ └── website.go │ └── rule │ │ ├── cron.go │ │ ├── exists.go │ │ ├── ip_cidr.go │ │ ├── not_exists.go │ │ ├── password.go │ │ └── rule.go ├── job │ ├── cert_renew.go │ ├── job.go │ ├── monitoring.go │ └── panel_task.go ├── migration │ ├── migration.go │ └── v1.go ├── queuejob │ └── process_task.go ├── route │ ├── cli.go │ ├── http.go │ ├── route.go │ └── ws.go └── service │ ├── app.go │ ├── backup.go │ ├── cert.go │ ├── cert_account.go │ ├── cert_dns.go │ ├── cli.go │ ├── container.go │ ├── container_compose.go │ ├── container_image.go │ ├── container_network.go │ ├── container_volume.go │ ├── cron.go │ ├── dashboard.go │ ├── database.go │ ├── database_server.go │ ├── database_user.go │ ├── file.go │ ├── file_windows.go │ ├── firewall.go │ ├── helper.go │ ├── monitor.go │ ├── process.go │ ├── safe.go │ ├── service.go │ ├── setting.go │ ├── ssh.go │ ├── systemctl.go │ ├── task.go │ ├── toolbox_benchmark.go │ ├── toolbox_system.go │ ├── user.go │ ├── user_token.go │ ├── website.go │ └── ws.go ├── mocks └── biz │ ├── AppRepo.go │ ├── BackupRepo.go │ ├── CacheRepo.go │ ├── CertAccountRepo.go │ ├── CertDNSRepo.go │ ├── CertRepo.go │ ├── ContainerComposeRepo.go │ ├── ContainerImageRepo.go │ ├── ContainerNetworkRepo.go │ ├── ContainerRepo.go │ ├── ContainerVolumeRepo.go │ ├── CronRepo.go │ ├── DatabaseRepo.go │ ├── DatabaseServerRepo.go │ ├── DatabaseUserRepo.go │ ├── MonitorRepo.go │ ├── SSHRepo.go │ ├── SafeRepo.go │ ├── SettingRepo.go │ ├── TaskRepo.go │ ├── UserRepo.go │ ├── UserTokenRepo.go │ └── WebsiteRepo.go ├── pkg ├── acme │ ├── acme.go │ ├── client.go │ ├── client_test.go │ └── solvers.go ├── api │ ├── acme.go │ ├── api.go │ ├── api_test.go │ ├── app.go │ ├── rewrite.go │ └── version.go ├── apploader │ └── apploader.go ├── cert │ ├── cert.go │ └── cert_test.go ├── chattr │ ├── chattr.go │ └── chattr_windows.go ├── cron │ └── logger.go ├── db │ ├── mysql.go │ ├── mysql_tools.go │ ├── postgres.go │ └── redis.go ├── embed │ ├── .gitignore │ ├── embed.go │ ├── frontend │ │ └── .gitkeep │ ├── locales │ │ ├── backend.pot │ │ ├── zh_CN │ │ │ └── backend.po │ │ └── zh_TW │ │ │ └── backend.po │ └── website │ │ ├── 404.html │ │ ├── 404_zh.html │ │ ├── index.html │ │ └── index_zh.html ├── firewall │ ├── consts.go │ └── firewall.go ├── io │ ├── compress.go │ ├── file.go │ ├── io_test.go │ └── path.go ├── nginx │ ├── data.go │ ├── getter.go │ ├── parser.go │ ├── parser_test.go │ ├── setter.go │ └── testdata │ │ ├── http.conf │ │ └── https.conf ├── ntp │ ├── ntp.go │ └── ntp_test.go ├── os │ ├── os.go │ ├── os_test.go │ └── user.go ├── punycode │ └── punycode.go ├── queue │ ├── job.go │ ├── queue.go │ └── queue_test.go ├── rsacrypto │ ├── rsacrypto.go │ └── rsacrypto_test.go ├── shell │ └── exec.go ├── ssh │ ├── ssh.go │ └── turn.go ├── systemctl │ └── service.go ├── tools │ ├── logger.go │ ├── tools.go │ └── tools_test.go └── types │ ├── app.go │ ├── backup.go │ ├── cert.go │ ├── common.go │ ├── config.go │ ├── container.go │ ├── container_compose.go │ ├── container_image.go │ ├── container_network.go │ ├── container_volume.go │ ├── docker │ ├── container │ │ └── container.go │ ├── image │ │ └── image.go │ ├── network │ │ ├── endpoint.go │ │ ├── ipam.go │ │ └── network.go │ ├── port.go │ ├── swarm │ │ └── meta.go │ └── volume │ │ ├── list_response.go │ │ └── volume.go │ ├── monitor.go │ ├── mysql.go │ ├── postgres.go │ ├── process.go │ ├── system.go │ └── website.go ├── renovate.json ├── storage └── logs │ └── .gitkeep └── web ├── .env.development ├── .env.production ├── .gitignore ├── .prettierrc.json ├── README.md ├── build ├── config │ ├── define.ts │ ├── index.ts │ └── proxy.ts ├── plugins │ ├── copy.ts │ ├── html.ts │ ├── index.ts │ └── unplugin.ts └── utils.ts ├── env.d.ts ├── eslint.config.js ├── gen-auto-import.ts ├── gettext.config.mjs ├── index.html ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── public ├── favicon.png ├── loading │ ├── index.css │ └── index.js └── robots.txt ├── settings ├── .gitignore ├── proxy-config.example.ts └── theme.json ├── src ├── App.vue ├── api │ ├── apps │ │ ├── codeserver │ │ │ └── index.ts │ │ ├── docker │ │ │ └── index.ts │ │ ├── fail2ban │ │ │ └── index.ts │ │ ├── frp │ │ │ └── index.ts │ │ ├── gitea │ │ │ └── index.ts │ │ ├── memcached │ │ │ └── index.ts │ │ ├── minio │ │ │ └── index.ts │ │ ├── mysql │ │ │ └── index.ts │ │ ├── nginx │ │ │ └── index.ts │ │ ├── php │ │ │ └── index.ts │ │ ├── phpmyadmin │ │ │ └── index.ts │ │ ├── podman │ │ │ └── index.ts │ │ ├── postgresql │ │ │ └── index.ts │ │ ├── pureftpd │ │ │ └── index.ts │ │ ├── redis │ │ │ └── index.ts │ │ ├── rsync │ │ │ └── index.ts │ │ ├── s3fs │ │ │ └── index.ts │ │ └── supervisor │ │ │ └── index.ts │ ├── panel │ │ ├── app │ │ │ └── index.ts │ │ ├── backup │ │ │ └── index.ts │ │ ├── cert │ │ │ └── index.ts │ │ ├── container │ │ │ └── index.ts │ │ ├── cron │ │ │ └── index.ts │ │ ├── dashboard │ │ │ └── index.ts │ │ ├── database │ │ │ └── index.ts │ │ ├── file │ │ │ └── index.ts │ │ ├── firewall │ │ │ └── index.ts │ │ ├── monitor │ │ │ └── index.ts │ │ ├── process │ │ │ └── index.ts │ │ ├── safe │ │ │ └── index.ts │ │ ├── setting │ │ │ └── index.ts │ │ ├── ssh │ │ │ └── index.ts │ │ ├── systemctl │ │ │ └── index.ts │ │ ├── task │ │ │ └── index.ts │ │ ├── toolbox-benchmark │ │ │ └── index.ts │ │ ├── toolbox-system │ │ │ └── index.ts │ │ ├── user │ │ │ └── index.ts │ │ └── website │ │ │ └── index.ts │ └── ws │ │ └── index.ts ├── assets │ └── images │ │ ├── 404.webp │ │ ├── login_bg.webp │ │ └── logo.png ├── components │ ├── common │ │ ├── AppFooter.vue │ │ ├── AppProvider.vue │ │ ├── CodeEditor.vue │ │ ├── PathSelector.vue │ │ ├── RealtimeLog.vue │ │ ├── RealtimeLogModal.vue │ │ └── ServiceStatus.vue │ ├── custom │ │ └── TheIcon.vue │ └── page │ │ ├── AppPage.vue │ │ └── CommonPage.vue ├── layout │ ├── AppMain.vue │ ├── IndexView.vue │ ├── header │ │ ├── IndexView.vue │ │ └── components │ │ │ ├── FullScreen.vue │ │ │ ├── MenuCollapse.vue │ │ │ ├── ReloadPage.vue │ │ │ ├── ThemeMode.vue │ │ │ ├── ThemeSetting.vue │ │ │ └── UserAvatar.vue │ ├── sidebar │ │ ├── IndexView.vue │ │ └── components │ │ │ ├── SideLogo.vue │ │ │ ├── SideMenu.vue │ │ │ └── SideSetting.vue │ └── tab │ │ ├── IndexView.vue │ │ └── components │ │ └── ContextMenu.vue ├── locales │ ├── .gitignore │ ├── en.po │ ├── frontend.pot │ ├── menu.ts │ ├── zh_CN.po │ └── zh_TW.po ├── main.ts ├── router │ ├── guard │ │ ├── app-install-guard.ts │ │ ├── index.ts │ │ ├── page-loading-guard.ts │ │ ├── page-title-guard.ts │ │ └── tab-guard.ts │ ├── index.ts │ └── routes │ │ └── index.ts ├── store │ ├── index.ts │ └── modules │ │ ├── app │ │ └── index.ts │ │ ├── file │ │ └── index.ts │ │ ├── index.ts │ │ ├── permission │ │ ├── helpers.ts │ │ └── index.ts │ │ ├── tab │ │ └── index.ts │ │ ├── theme │ │ ├── helpers.ts │ │ └── index.ts │ │ └── user │ │ └── index.ts ├── styles │ ├── index.scss │ └── reset.css ├── utils │ ├── auth │ │ ├── index.ts │ │ └── router.ts │ ├── common │ │ ├── base64.ts │ │ ├── color.ts │ │ ├── common.ts │ │ ├── icon.ts │ │ ├── index.ts │ │ ├── is.ts │ │ └── naiveTools.ts │ ├── encrypt │ │ ├── index.ts │ │ └── rsa.ts │ ├── file │ │ └── index.ts │ ├── gettext │ │ └── index.ts │ ├── http │ │ ├── helpers.ts │ │ └── index.ts │ ├── index.ts │ └── storage │ │ ├── index.ts │ │ └── local.ts └── views │ ├── app │ ├── IndexView.vue │ ├── VersionModal.vue │ ├── route.ts │ └── types.ts │ ├── apps │ ├── codeserver │ │ ├── IndexView.vue │ │ └── route.ts │ ├── docker │ │ ├── IndexView.vue │ │ └── route.ts │ ├── fail2ban │ │ ├── IndexView.vue │ │ └── route.ts │ ├── frp │ │ ├── IndexView.vue │ │ └── route.ts │ ├── gitea │ │ ├── IndexView.vue │ │ └── route.ts │ ├── memcached │ │ ├── IndexView.vue │ │ └── route.ts │ ├── minio │ │ ├── IndexView.vue │ │ └── route.ts │ ├── mysql │ │ ├── IndexView.vue │ │ └── route.ts │ ├── nginx │ │ ├── IndexView.vue │ │ ├── route.ts │ │ └── types.ts │ ├── php │ │ └── PhpView.vue │ ├── php74 │ │ ├── IndexView.vue │ │ └── route.ts │ ├── php80 │ │ ├── IndexView.vue │ │ └── route.ts │ ├── php81 │ │ ├── IndexView.vue │ │ └── route.ts │ ├── php82 │ │ ├── IndexView.vue │ │ └── route.ts │ ├── php83 │ │ ├── IndexView.vue │ │ └── route.ts │ ├── php84 │ │ ├── IndexView.vue │ │ └── route.ts │ ├── phpmyadmin │ │ ├── IndexView.vue │ │ └── route.ts │ ├── podman │ │ ├── IndexView.vue │ │ └── route.ts │ ├── postgresql │ │ ├── IndexView.vue │ │ └── route.ts │ ├── pureftpd │ │ ├── IndexView.vue │ │ └── route.ts │ ├── redis │ │ ├── IndexView.vue │ │ └── route.ts │ ├── rsync │ │ ├── IndexView.vue │ │ └── route.ts │ ├── s3fs │ │ ├── IndexView.vue │ │ └── route.ts │ └── supervisor │ │ ├── IndexView.vue │ │ └── route.ts │ ├── backup │ ├── IndexView.vue │ ├── ListView.vue │ ├── UploadModal.vue │ └── route.ts │ ├── cert │ ├── AccountView.vue │ ├── CertView.vue │ ├── CreateAccountModal.vue │ ├── CreateCertModal.vue │ ├── CreateDnsModal.vue │ ├── DnsView.vue │ ├── IndexView.vue │ ├── ObtainModal.vue │ ├── UploadCertModal.vue │ └── route.ts │ ├── container │ ├── ComposeView.vue │ ├── ContainerCreate.vue │ ├── ContainerView.vue │ ├── ImageView.vue │ ├── IndexView.vue │ ├── NetworkView.vue │ ├── VolumeView.vue │ └── route.ts │ ├── dashboard │ ├── IndexView.vue │ ├── UpdateView.vue │ ├── route.ts │ └── types.ts │ ├── database │ ├── CreateDatabaseModal.vue │ ├── CreateServerModal.vue │ ├── CreateUserModal.vue │ ├── DatabaseList.vue │ ├── IndexView.vue │ ├── ServerList.vue │ ├── UpdateServerModal.vue │ ├── UpdateUserModal.vue │ ├── UserList.vue │ └── route.ts │ ├── error-page │ └── NotFound.vue │ ├── file │ ├── CompressModal.vue │ ├── EditModal.vue │ ├── IndexView.vue │ ├── ListTable.vue │ ├── PathInput.vue │ ├── PermissionModal.vue │ ├── PreviewModal.vue │ ├── SearchModal.vue │ ├── ToolBar.vue │ ├── UploadModal.vue │ ├── route.ts │ └── types.ts │ ├── firewall │ ├── CreateForwardModal.vue │ ├── CreateIpModal.vue │ ├── CreateModal.vue │ ├── ForwardView.vue │ ├── IndexView.vue │ ├── IpRuleView.vue │ ├── RuleView.vue │ ├── SettingView.vue │ └── route.ts │ ├── login │ └── IndexView.vue │ ├── monitor │ ├── IndexView.vue │ └── route.ts │ ├── setting │ ├── CreateModal.vue │ ├── IndexView.vue │ ├── PasswordModal.vue │ ├── SettingBase.vue │ ├── SettingSafe.vue │ ├── SettingUser.vue │ ├── TokenModal.vue │ ├── TwoFaModal.vue │ ├── route.ts │ └── types.ts │ ├── ssh │ ├── CreateModal.vue │ ├── IndexView.vue │ ├── UpdateModal.vue │ └── route.ts │ ├── task │ ├── CreateModal.vue │ ├── CronView.vue │ ├── IndexView.vue │ ├── SystemView.vue │ ├── TaskView.vue │ └── route.ts │ ├── toolbox │ ├── BenchmarkView.vue │ ├── SystemView.vue │ └── route.ts │ └── website │ ├── BulkCreate.vue │ ├── EditView.vue │ ├── IndexView.vue │ ├── ProxyBuilderModal.vue │ └── route.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── types ├── env.d.ts ├── global.d.ts ├── router.d.ts ├── shims.d.ts └── theme.d.ts ├── uno.config.ts └── vite.config.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sh linguist-language=Go 2 | *.ts linguist-language=Go 3 | *.js linguist-language=Go 4 | *.css linguist-language=Go 5 | *.scss linguist-language=Go 6 | *.html linguist-language=Go -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: [ 'https://afdian.com/a/tnblabs' ] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: ❓ 讨论、问答和非项目问题 (Discussions, questions, and non-project issues) 4 | url: https://jq.qq.com/?_wv=1027&k=I1oJKSTH 5 | about: 其他不明之处,请移步我们的QQ群 12370907 (For other unclear things, please move to our qq group 12370907) 6 | -------------------------------------------------------------------------------- /.github/assets/ddunyun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tnb-labs/panel/f3cec5e476fbe0fdbd6567ea67ca2bfa1745c258/.github/assets/ddunyun.png -------------------------------------------------------------------------------- /.github/assets/dk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tnb-labs/panel/f3cec5e476fbe0fdbd6567ea67ca2bfa1745c258/.github/assets/dk.png -------------------------------------------------------------------------------- /.github/assets/ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tnb-labs/panel/f3cec5e476fbe0fdbd6567ea67ca2bfa1745c258/.github/assets/ui.png -------------------------------------------------------------------------------- /.github/assets/wafpro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tnb-labs/panel/f3cec5e476fbe0fdbd6567ea67ca2bfa1745c258/.github/assets/wafpro.png -------------------------------------------------------------------------------- /.github/assets/wxd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tnb-labs/panel/f3cec5e476fbe0fdbd6567ea67ca2bfa1745c258/.github/assets/wxd.png -------------------------------------------------------------------------------- /.github/workflows/issue-auto-close.yml: -------------------------------------------------------------------------------- 1 | name: Issue Auto Lock 2 | on: 3 | schedule: 4 | - cron: "*/5 * * * *" 5 | workflow_dispatch: 6 | issues: 7 | types: [ opened ] 8 | permissions: 9 | issues: write 10 | contents: read 11 | jobs: 12 | issue-lock: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Set GH_REPO 16 | run: echo "GH_REPO=${{ github.repository }}" >> $GITHUB_ENV 17 | - name: Auto Lock Issue 18 | uses: devhaozi/issue-auto-lock@v1 19 | with: 20 | gh_repo: ${{ github.repository }} 21 | gh_token: ${{ secrets.GITHUB_TOKEN }} 22 | issue_labels: "⭐ No Star" 23 | -------------------------------------------------------------------------------- /.github/workflows/issue-auto-reply.yml: -------------------------------------------------------------------------------- 1 | name: Issue Auto Reply 2 | on: 3 | issues: 4 | types: [ labeled ] 5 | permissions: 6 | contents: read 7 | jobs: 8 | issue-reply: 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: ☢️ Bug 15 | if: github.event.label.name == '☢️ Bug' 16 | uses: actions-cool/issues-helper@v3 17 | with: 18 | actions: 'create-comment' 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | issue-number: ${{ github.event.issue.number }} 21 | body: | 22 | Hi @${{ github.event.issue.user.login }} 👋 23 | 24 | 感谢提交 Bug,请确保您的描述清晰,日志完整,最好能提供复现步骤,以便我们更快定位问题并解决。 25 | Thanks for submitting a bug, please make sure your description is clear, log is complete, and it is best to provide a reproduction step so that we can locate and solve the problem faster. 26 | 27 |  28 | -------------------------------------------------------------------------------- /.github/workflows/l10n.yml: -------------------------------------------------------------------------------- 1 | name: L10n 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | l10n: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v4 12 | - name: Setup pnpm 13 | uses: pnpm/action-setup@v4 14 | with: 15 | version: latest 16 | run_install: true 17 | package_json_file: web/package.json 18 | - name: Setup Node.js 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: 22 22 | cache: 'pnpm' 23 | cache-dependency-path: web/pnpm-lock.yaml 24 | - name: Setup Go 25 | uses: actions/setup-go@v5 26 | with: 27 | cache: true 28 | go-version: 'stable' 29 | - name: Install xgotext 30 | run: | 31 | go install github.com/leonelquinteros/gotext/cli/xgotext@latest 32 | - name: Generate pot files 33 | run: | 34 | ~/go/bin/xgotext -default backend -pkg-tree ./cmd/web -out ./pkg/embed/locales 35 | cd web && pnpm run gettext:extract 36 | - uses: stefanzweifel/git-auto-commit-action@v5 37 | name: Commit changes 38 | with: 39 | commit_message: "chore(l10n): update pot files" 40 | -------------------------------------------------------------------------------- /.github/workflows/mockery.yml: -------------------------------------------------------------------------------- 1 | name: Mockery 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | mockery: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v4 12 | - name: Setup Go 13 | uses: actions/setup-go@v5 14 | with: 15 | cache: true 16 | go-version: 'stable' 17 | - name: Install Mockery 18 | run: | 19 | go install github.com/vektra/mockery/v2@latest 20 | - name: Generate Mocks 21 | run: | 22 | ~/go/bin/mockery 23 | git pull 24 | - uses: stefanzweifel/git-auto-commit-action@v5 25 | name: Commit changes 26 | with: 27 | commit_message: "chore: update mocks" 28 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | unit: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | - name: Setup Go 14 | uses: actions/setup-go@v5 15 | with: 16 | cache: true 17 | go-version: 'stable' 18 | - name: Install dependencies 19 | run: sudo apt-get install -y curl jq 20 | - name: Set up environment 21 | run: | 22 | cp config.example.yml config.yml 23 | - name: Run tests 24 | run: sudo go test -v -coverprofile="coverage.out" ./... 25 | - name: Upload coverage report to Codecov 26 | uses: codecov/codecov-action@v5 27 | with: 28 | file: ./coverage.out 29 | token: ${{ secrets.CODECOV }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # `go test -c` 生成的二进制文件 2 | *.test 3 | 4 | # go coverage 工具 5 | *.out 6 | *.prof 7 | *.cgo1.go 8 | *.cgo2.c 9 | _cgo_defun.c 10 | _cgo_gotypes.go 11 | _cgo_export.* 12 | 13 | # 编译文件 14 | *.com 15 | *.class 16 | *.dll 17 | *.exe 18 | *.o 19 | *.so 20 | 21 | # 压缩包 22 | # Git 自带压缩,如果这些压缩包里有代码,建议解压后 commit 23 | *.7z 24 | *.dmg 25 | *.gz 26 | *.iso 27 | *.jar 28 | *.rar 29 | *.tar 30 | *.zip 31 | 32 | # 日志文件和数据库及配置 33 | *.log 34 | *.sqlite 35 | *.db 36 | config.yml 37 | 38 | # 临时文件 39 | tmp/ 40 | .tmp/ 41 | 42 | # 系统生成文件 43 | .DS_Store 44 | .DS_Store? 45 | .AppleDouble 46 | .LSOverride 47 | ._* 48 | .Spotlight-V100 49 | .Trashes 50 | ehthumbs.db 51 | Thumbs.db 52 | .TemporaryItems 53 | .fseventsd 54 | .VolumeIcon.icns 55 | .com.apple.timemachine.donotpresent 56 | 57 | # IDE 和编辑器 58 | .idea/ 59 | /go_build_* 60 | out/ 61 | .vscode/ 62 | .vscode/settings.json 63 | *.sublime* 64 | __debug_bin 65 | .project 66 | 67 | # 傻逼 node_modules 68 | node_modules/ 69 | -------------------------------------------------------------------------------- /.mockery.yaml: -------------------------------------------------------------------------------- 1 | with-expecter: True 2 | disable-version-string: True 3 | dir: mocks/{{ replaceAll .InterfaceDirRelative "internal" "" }} 4 | mockname: "{{.InterfaceName}}" 5 | outpkg: "{{.PackageName}}" 6 | filename: "{{.InterfaceName}}.go" 7 | all: True 8 | packages: 9 | github.com/tnb-labs/panel/internal/biz: 10 | config: 11 | recursive: True 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## 行为准则 2 | 3 | 耗子面板遵守业界通用的行为准则。任何违反行为准则的行为都可以报告给我们: 4 | 5 | - 参与者将容忍反对意见。 6 | - 参与者必须确保他们的语言和行为没有人身攻击和贬低个人言论。 7 | - 在解释他人的言行时,参与者应始终保持良好的意图。 8 | - 不能容忍可合理视为骚扰的行为。 9 | 10 | ## Code of Conduct 11 | 12 | The Rat Panel complies with the industry's common code of conduct. Any breach of the Code of Conduct can be reported to us: 13 | 14 | - Participants will be tolerant of opposing views. 15 | - Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks. 16 | - When interpreting the words and actions of others, participants should always assume good intentions. 17 | - Behavior that can be reasonably considered harassment will not be tolerated. 18 | -------------------------------------------------------------------------------- /cmd/README.md: -------------------------------------------------------------------------------- 1 | # cmd 2 | 3 | cmd 目录存放应用的入口文件。 -------------------------------------------------------------------------------- /cmd/cli/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2022 - now Rat Technology Co., Ltd. 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published 6 | by the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with this program. If not, see . 16 | */ 17 | package main 18 | 19 | import ( 20 | "os" 21 | _ "time/tzdata" 22 | ) 23 | 24 | func main() { 25 | if os.Geteuid() != 0 { 26 | panic("panel must run as root") 27 | } 28 | 29 | cli, err := initCli() 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | if err = cli.Run(); err != nil { 35 | panic(err) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /cmd/cli/wire.go: -------------------------------------------------------------------------------- 1 | //go:build wireinject 2 | 3 | package main 4 | 5 | import ( 6 | "github.com/google/wire" 7 | 8 | "github.com/tnb-labs/panel/internal/app" 9 | "github.com/tnb-labs/panel/internal/apps" 10 | "github.com/tnb-labs/panel/internal/bootstrap" 11 | "github.com/tnb-labs/panel/internal/data" 12 | "github.com/tnb-labs/panel/internal/route" 13 | "github.com/tnb-labs/panel/internal/service" 14 | ) 15 | 16 | // initCli init command line. 17 | func initCli() (*app.Cli, error) { 18 | panic(wire.Build(bootstrap.ProviderSet, route.ProviderSet, service.ProviderSet, data.ProviderSet, apps.ProviderSet, app.NewCli)) 19 | } 20 | -------------------------------------------------------------------------------- /cmd/web/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2022 - now Rat Technology Co., Ltd. 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published 6 | by the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with this program. If not, see . 16 | */ 17 | package main 18 | 19 | import ( 20 | "os" 21 | "runtime/debug" 22 | _ "time/tzdata" 23 | ) 24 | 25 | func main() { 26 | if os.Geteuid() != 0 { 27 | panic("panel must run as root") 28 | } 29 | 30 | debug.SetGCPercent(10) 31 | debug.SetMemoryLimit(128 << 20) 32 | 33 | web, err := initWeb() 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | if err = web.Run(); err != nil { 39 | panic(err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /cmd/web/wire.go: -------------------------------------------------------------------------------- 1 | //go:build wireinject 2 | 3 | package main 4 | 5 | import ( 6 | "github.com/google/wire" 7 | 8 | "github.com/tnb-labs/panel/internal/app" 9 | "github.com/tnb-labs/panel/internal/apps" 10 | "github.com/tnb-labs/panel/internal/bootstrap" 11 | "github.com/tnb-labs/panel/internal/data" 12 | "github.com/tnb-labs/panel/internal/http/middleware" 13 | "github.com/tnb-labs/panel/internal/job" 14 | "github.com/tnb-labs/panel/internal/route" 15 | "github.com/tnb-labs/panel/internal/service" 16 | ) 17 | 18 | // initWeb init application. 19 | func initWeb() (*app.Web, error) { 20 | panic(wire.Build(bootstrap.ProviderSet, middleware.ProviderSet, route.ProviderSet, service.ProviderSet, data.ProviderSet, apps.ProviderSet, job.ProviderSet, app.NewWeb)) 21 | } 22 | -------------------------------------------------------------------------------- /config.example.yml: -------------------------------------------------------------------------------- 1 | app: 2 | debug: false 3 | key: a-long-string-with-32-characters 4 | locale: zh_CN 5 | timezone: Asia/Shanghai 6 | root: /www 7 | http: 8 | debug: false 9 | port: 8888 10 | entrance: / 11 | tls: true 12 | database: 13 | debug: false 14 | session: 15 | lifetime: 120 16 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | commit_message: 'Update translations (%language%) %original_file_name%' 2 | pull_request_title: 'l10n: update translations' 3 | files: 4 | - source: /pkg/embed/locales/*.pot 5 | translation: /pkg/embed/locales/%locale_with_underscore%/%file_name%.po 6 | - source: /web/src/locales/*.pot 7 | translation: /web/src/locales/%locale_with_underscore%.po 8 | -------------------------------------------------------------------------------- /internal/app/cli.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/go-gormigrate/gormigrate/v2" 8 | "github.com/gookit/color" 9 | "github.com/urfave/cli/v3" 10 | 11 | "github.com/tnb-labs/panel/pkg/apploader" 12 | ) 13 | 14 | type Cli struct { 15 | cmd *cli.Command 16 | migrator *gormigrate.Gormigrate 17 | } 18 | 19 | func NewCli(cmd *cli.Command, migrator *gormigrate.Gormigrate, _ *apploader.Loader) *Cli { 20 | IsCli = true 21 | return &Cli{ 22 | cmd: cmd, 23 | migrator: migrator, 24 | } 25 | } 26 | 27 | func (r *Cli) Run() error { 28 | // migrate database 29 | // 这里不处理错误,这么做是为了在异常时用户可以用 fix 命令尝试修复 30 | _ = r.migrator.Migrate() 31 | 32 | if err := r.cmd.Run(context.TODO(), os.Args); err != nil { 33 | color.Errorf("|-%v\n", err) 34 | } 35 | 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /internal/app/global.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | // 面板状态常量 4 | const ( 5 | StatusNormal = iota 6 | StatusMaintain 7 | StatusClosed 8 | StatusUpgrade 9 | StatusFailed 10 | ) 11 | 12 | // 面板全局变量 13 | var ( 14 | Key string // 密钥 15 | Root string // 根目录 16 | Locale string // 语言 17 | IsCli bool // 是否命令行 18 | Status = StatusNormal // 面板状态 19 | ) 20 | 21 | // 自动注入 22 | var ( 23 | Version = "0.0.0" 24 | BuildTime string 25 | CommitHash string 26 | GoVersion string 27 | BuildID string 28 | BuildUser string 29 | BuildHost string 30 | ) 31 | -------------------------------------------------------------------------------- /internal/apps/codeserver/app.go: -------------------------------------------------------------------------------- 1 | package codeserver 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-chi/chi/v5" 7 | 8 | "github.com/tnb-labs/panel/internal/service" 9 | "github.com/tnb-labs/panel/pkg/io" 10 | "github.com/tnb-labs/panel/pkg/systemctl" 11 | ) 12 | 13 | type App struct{} 14 | 15 | func NewApp() *App { 16 | return &App{} 17 | } 18 | 19 | func (s *App) Route(r chi.Router) { 20 | r.Get("/config", s.GetConfig) 21 | r.Post("/config", s.UpdateConfig) 22 | } 23 | 24 | func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) { 25 | config, _ := io.Read("/root/.config/code-server/config.yaml") 26 | service.Success(w, config) 27 | } 28 | 29 | func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) { 30 | req, err := service.Bind[UpdateConfig](r) 31 | if err != nil { 32 | service.Error(w, http.StatusUnprocessableEntity, "%v", err) 33 | return 34 | } 35 | 36 | if err = io.Write("/root/.config/code-server/config.yaml", req.Config, 0600); err != nil { 37 | service.Error(w, http.StatusInternalServerError, "%v", err) 38 | return 39 | } 40 | 41 | if err = systemctl.Restart("code-server"); err != nil { 42 | service.Error(w, http.StatusInternalServerError, "%v", err) 43 | return 44 | } 45 | 46 | service.Success(w, nil) 47 | } 48 | -------------------------------------------------------------------------------- /internal/apps/codeserver/request.go: -------------------------------------------------------------------------------- 1 | package codeserver 2 | 3 | type UpdateConfig struct { 4 | Config string `form:"config" json:"config" validate:"required"` 5 | } 6 | -------------------------------------------------------------------------------- /internal/apps/docker/request.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | type UpdateConfig struct { 4 | Config string `form:"config" json:"config" validate:"required"` 5 | } 6 | -------------------------------------------------------------------------------- /internal/apps/fail2ban/request.go: -------------------------------------------------------------------------------- 1 | package fail2ban 2 | 3 | type Add struct { 4 | Name string `json:"name" validate:"required"` 5 | Type string `json:"type" validate:"required"` 6 | MaxRetry int `json:"maxretry" validate:"required"` 7 | FindTime int `json:"findtime" validate:"required"` 8 | BanTime int `json:"bantime" validate:"required"` 9 | WebsiteName string `json:"website_name"` 10 | WebsiteMode string `json:"website_mode"` 11 | WebsitePath string `json:"website_path"` 12 | } 13 | 14 | type Delete struct { 15 | Name string `json:"name" validate:"required"` 16 | } 17 | 18 | type BanList struct { 19 | Name string `json:"name" validate:"required"` 20 | } 21 | 22 | type Unban struct { 23 | Name string `json:"name" validate:"required"` 24 | IP string `json:"ip" validate:"required"` 25 | } 26 | 27 | type SetWhiteList struct { 28 | IP string `json:"ip" validate:"required"` 29 | } 30 | -------------------------------------------------------------------------------- /internal/apps/fail2ban/types.go: -------------------------------------------------------------------------------- 1 | package fail2ban 2 | 3 | type Jail struct { 4 | Name string `json:"name"` 5 | Enabled bool `json:"enabled"` 6 | MaxRetry int `json:"max_retry"` 7 | FindTime int `json:"find_time"` 8 | BanTime int `json:"ban_time"` 9 | } 10 | -------------------------------------------------------------------------------- /internal/apps/frp/request.go: -------------------------------------------------------------------------------- 1 | package frp 2 | 3 | type Name struct { 4 | Name string `form:"name" json:"name" validate:"required"` 5 | } 6 | 7 | type UpdateConfig struct { 8 | Name string `form:"name" json:"name" validate:"required"` 9 | Config string `form:"config" json:"config" validate:"required"` 10 | } 11 | -------------------------------------------------------------------------------- /internal/apps/gitea/app.go: -------------------------------------------------------------------------------- 1 | package gitea 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/go-chi/chi/v5" 8 | 9 | "github.com/tnb-labs/panel/internal/app" 10 | "github.com/tnb-labs/panel/internal/service" 11 | "github.com/tnb-labs/panel/pkg/io" 12 | "github.com/tnb-labs/panel/pkg/systemctl" 13 | ) 14 | 15 | type App struct{} 16 | 17 | func NewApp() *App { 18 | return &App{} 19 | } 20 | 21 | func (s *App) Route(r chi.Router) { 22 | r.Get("/config", s.GetConfig) 23 | r.Post("/config", s.UpdateConfig) 24 | } 25 | 26 | func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) { 27 | config, _ := io.Read(fmt.Sprintf("%s/server/gitea/app.ini", app.Root)) 28 | service.Success(w, config) 29 | } 30 | 31 | func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) { 32 | req, err := service.Bind[UpdateConfig](r) 33 | if err != nil { 34 | service.Error(w, http.StatusUnprocessableEntity, "%v", err) 35 | return 36 | } 37 | 38 | if err = io.Write(fmt.Sprintf("%s/server/gitea/app.ini", app.Root), req.Config, 0644); err != nil { 39 | service.Error(w, http.StatusInternalServerError, "%v", err) 40 | return 41 | } 42 | 43 | if err = systemctl.Restart("gitea"); err != nil { 44 | service.Error(w, http.StatusInternalServerError, "%v", err) 45 | return 46 | } 47 | 48 | service.Success(w, nil) 49 | } 50 | -------------------------------------------------------------------------------- /internal/apps/gitea/request.go: -------------------------------------------------------------------------------- 1 | package gitea 2 | 3 | type UpdateConfig struct { 4 | Config string `form:"config" json:"config" validate:"required"` 5 | } 6 | -------------------------------------------------------------------------------- /internal/apps/memcached/request.go: -------------------------------------------------------------------------------- 1 | package memcached 2 | 3 | type UpdateConfig struct { 4 | Config string `form:"config" json:"config" validate:"required"` 5 | } 6 | -------------------------------------------------------------------------------- /internal/apps/minio/app.go: -------------------------------------------------------------------------------- 1 | package minio 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-chi/chi/v5" 7 | 8 | "github.com/tnb-labs/panel/internal/service" 9 | "github.com/tnb-labs/panel/pkg/io" 10 | "github.com/tnb-labs/panel/pkg/systemctl" 11 | ) 12 | 13 | type App struct{} 14 | 15 | func NewApp() *App { 16 | return &App{} 17 | } 18 | 19 | func (s *App) Route(r chi.Router) { 20 | r.Get("/env", s.GetEnv) 21 | r.Post("/env", s.UpdateEnv) 22 | } 23 | 24 | func (s *App) GetEnv(w http.ResponseWriter, r *http.Request) { 25 | env, _ := io.Read("/etc/default/minio") 26 | service.Success(w, env) 27 | } 28 | 29 | func (s *App) UpdateEnv(w http.ResponseWriter, r *http.Request) { 30 | req, err := service.Bind[UpdateEnv](r) 31 | if err != nil { 32 | service.Error(w, http.StatusUnprocessableEntity, "%v", err) 33 | return 34 | } 35 | 36 | if err = io.Write("/etc/default/minio", req.Env, 0600); err != nil { 37 | service.Error(w, http.StatusInternalServerError, "%v", err) 38 | return 39 | } 40 | 41 | if err = systemctl.Restart("minio"); err != nil { 42 | service.Error(w, http.StatusInternalServerError, "%v", err) 43 | return 44 | } 45 | 46 | service.Success(w, nil) 47 | } 48 | -------------------------------------------------------------------------------- /internal/apps/minio/request.go: -------------------------------------------------------------------------------- 1 | package minio 2 | 3 | type UpdateEnv struct { 4 | Env string `form:"env" json:"env" validate:"required"` 5 | } 6 | -------------------------------------------------------------------------------- /internal/apps/mysql/request.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | type UpdateConfig struct { 4 | Config string `form:"config" json:"config" validate:"required"` 5 | } 6 | 7 | type SetRootPassword struct { 8 | Password string `form:"password" json:"password" validate:"required|password"` 9 | } 10 | -------------------------------------------------------------------------------- /internal/apps/nginx/request.go: -------------------------------------------------------------------------------- 1 | package nginx 2 | 3 | type UpdateConfig struct { 4 | Config string `form:"config" json:"config" validate:"required"` 5 | } 6 | -------------------------------------------------------------------------------- /internal/apps/php/request.go: -------------------------------------------------------------------------------- 1 | package php 2 | 3 | type UpdateConfig struct { 4 | Config string `form:"config" json:"config" validate:"required"` 5 | } 6 | 7 | type ExtensionSlug struct { 8 | Slug string `form:"slug" json:"slug" validate:"required"` 9 | } 10 | -------------------------------------------------------------------------------- /internal/apps/php/types.go: -------------------------------------------------------------------------------- 1 | package php 2 | 3 | type Extension struct { 4 | Name string `json:"name"` 5 | Slug string `json:"slug"` 6 | Description string `json:"description"` 7 | Installed bool `json:"installed"` 8 | } 9 | -------------------------------------------------------------------------------- /internal/apps/php74/app.go: -------------------------------------------------------------------------------- 1 | package php74 2 | 3 | import ( 4 | "github.com/go-chi/chi/v5" 5 | "github.com/leonelquinteros/gotext" 6 | 7 | "github.com/tnb-labs/panel/internal/apps/php" 8 | "github.com/tnb-labs/panel/internal/biz" 9 | ) 10 | 11 | type App struct { 12 | php *php.App 13 | } 14 | 15 | func NewApp(t *gotext.Locale, task biz.TaskRepo) *App { 16 | return &App{ 17 | php: php.NewApp(t, task), 18 | } 19 | } 20 | 21 | func (s *App) Route(r chi.Router) { 22 | s.php.Route(74)(r) 23 | } 24 | -------------------------------------------------------------------------------- /internal/apps/php80/app.go: -------------------------------------------------------------------------------- 1 | package php80 2 | 3 | import ( 4 | "github.com/go-chi/chi/v5" 5 | "github.com/leonelquinteros/gotext" 6 | 7 | "github.com/tnb-labs/panel/internal/apps/php" 8 | "github.com/tnb-labs/panel/internal/biz" 9 | ) 10 | 11 | type App struct { 12 | php *php.App 13 | } 14 | 15 | func NewApp(t *gotext.Locale, task biz.TaskRepo) *App { 16 | return &App{ 17 | php: php.NewApp(t, task), 18 | } 19 | } 20 | 21 | func (s *App) Route(r chi.Router) { 22 | s.php.Route(80)(r) 23 | } 24 | -------------------------------------------------------------------------------- /internal/apps/php81/app.go: -------------------------------------------------------------------------------- 1 | package php81 2 | 3 | import ( 4 | "github.com/go-chi/chi/v5" 5 | "github.com/leonelquinteros/gotext" 6 | 7 | "github.com/tnb-labs/panel/internal/apps/php" 8 | "github.com/tnb-labs/panel/internal/biz" 9 | ) 10 | 11 | type App struct { 12 | php *php.App 13 | } 14 | 15 | func NewApp(t *gotext.Locale, task biz.TaskRepo) *App { 16 | return &App{ 17 | php: php.NewApp(t, task), 18 | } 19 | } 20 | 21 | func (s *App) Route(r chi.Router) { 22 | s.php.Route(81)(r) 23 | } 24 | -------------------------------------------------------------------------------- /internal/apps/php82/app.go: -------------------------------------------------------------------------------- 1 | package php82 2 | 3 | import ( 4 | "github.com/go-chi/chi/v5" 5 | "github.com/leonelquinteros/gotext" 6 | 7 | "github.com/tnb-labs/panel/internal/apps/php" 8 | "github.com/tnb-labs/panel/internal/biz" 9 | ) 10 | 11 | type App struct { 12 | php *php.App 13 | } 14 | 15 | func NewApp(t *gotext.Locale, task biz.TaskRepo) *App { 16 | return &App{ 17 | php: php.NewApp(t, task), 18 | } 19 | } 20 | 21 | func (s *App) Route(r chi.Router) { 22 | s.php.Route(82)(r) 23 | } 24 | -------------------------------------------------------------------------------- /internal/apps/php83/app.go: -------------------------------------------------------------------------------- 1 | package php83 2 | 3 | import ( 4 | "github.com/go-chi/chi/v5" 5 | "github.com/leonelquinteros/gotext" 6 | 7 | "github.com/tnb-labs/panel/internal/apps/php" 8 | "github.com/tnb-labs/panel/internal/biz" 9 | ) 10 | 11 | type App struct { 12 | php *php.App 13 | } 14 | 15 | func NewApp(t *gotext.Locale, task biz.TaskRepo) *App { 16 | return &App{ 17 | php: php.NewApp(t, task), 18 | } 19 | } 20 | 21 | func (s *App) Route(r chi.Router) { 22 | s.php.Route(83)(r) 23 | } 24 | -------------------------------------------------------------------------------- /internal/apps/php84/app.go: -------------------------------------------------------------------------------- 1 | package php84 2 | 3 | import ( 4 | "github.com/go-chi/chi/v5" 5 | "github.com/leonelquinteros/gotext" 6 | 7 | "github.com/tnb-labs/panel/internal/apps/php" 8 | "github.com/tnb-labs/panel/internal/biz" 9 | ) 10 | 11 | type App struct { 12 | php *php.App 13 | } 14 | 15 | func NewApp(t *gotext.Locale, task biz.TaskRepo) *App { 16 | return &App{ 17 | php: php.NewApp(t, task), 18 | } 19 | } 20 | 21 | func (s *App) Route(r chi.Router) { 22 | s.php.Route(84)(r) 23 | } 24 | -------------------------------------------------------------------------------- /internal/apps/phpmyadmin/request.go: -------------------------------------------------------------------------------- 1 | package phpmyadmin 2 | 3 | type UpdateConfig struct { 4 | Config string `form:"config" json:"config" validate:"required"` 5 | } 6 | 7 | type UpdatePort struct { 8 | Port uint `form:"port" json:"port" validate:"required|number|min:1|max:65535"` 9 | } 10 | -------------------------------------------------------------------------------- /internal/apps/podman/request.go: -------------------------------------------------------------------------------- 1 | package podman 2 | 3 | type UpdateConfig struct { 4 | Config string `form:"config" json:"config" validate:"required"` 5 | } 6 | -------------------------------------------------------------------------------- /internal/apps/postgresql/request.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | type UpdateConfig struct { 4 | Config string `form:"config" json:"config" validate:"required"` 5 | } 6 | -------------------------------------------------------------------------------- /internal/apps/pureftpd/request.go: -------------------------------------------------------------------------------- 1 | package pureftpd 2 | 3 | type Create struct { 4 | Username string `form:"username" json:"username" validate:"required"` 5 | Password string `form:"password" json:"password" validate:"required|password"` 6 | Path string `form:"path" json:"path" validate:"required"` 7 | } 8 | 9 | type Delete struct { 10 | Username string `form:"username" json:"username" validate:"required"` 11 | } 12 | 13 | type ChangePassword struct { 14 | Username string `form:"username" json:"username" validate:"required"` 15 | Password string `form:"password" json:"password" validate:"required|password"` 16 | } 17 | 18 | type UpdatePort struct { 19 | Port uint `form:"port" json:"port" validate:"required|number|min:1|max:65535"` 20 | } 21 | -------------------------------------------------------------------------------- /internal/apps/pureftpd/types.go: -------------------------------------------------------------------------------- 1 | package pureftpd 2 | 3 | type User struct { 4 | Username string `json:"username"` 5 | Path string `json:"path"` 6 | } 7 | -------------------------------------------------------------------------------- /internal/apps/redis/request.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | type UpdateConfig struct { 4 | Config string `form:"config" json:"config" validate:"required"` 5 | } 6 | -------------------------------------------------------------------------------- /internal/apps/rsync/request.go: -------------------------------------------------------------------------------- 1 | package rsync 2 | 3 | type Create struct { 4 | Name string `form:"name" json:"name" validate:"required"` 5 | Path string `form:"path" json:"path" validate:"required"` 6 | Comment string `form:"comment" json:"comment"` 7 | AuthUser string `form:"auth_user" json:"auth_user" validate:"required"` 8 | Secret string `form:"secret" json:"secret" validate:"required"` 9 | HostsAllow string `form:"hosts_allow" json:"hosts_allow"` 10 | } 11 | 12 | type Delete struct { 13 | Name string `form:"name" json:"name" validate:"required"` 14 | } 15 | 16 | type Update struct { 17 | Name string `form:"name" json:"name" validate:"required"` 18 | Path string `form:"path" json:"path" validate:"required"` 19 | Comment string `form:"comment" json:"comment"` 20 | AuthUser string `form:"auth_user" json:"auth_user" validate:"required"` 21 | Secret string `form:"secret" json:"secret" validate:"required"` 22 | HostsAllow string `form:"hosts_allow" json:"hosts_allow"` 23 | } 24 | 25 | type UpdateConfig struct { 26 | Config string `form:"config" json:"config" validate:"required"` 27 | } 28 | -------------------------------------------------------------------------------- /internal/apps/rsync/types.go: -------------------------------------------------------------------------------- 1 | package rsync 2 | 3 | type Module struct { 4 | Name string `json:"name"` 5 | Path string `json:"path"` 6 | Comment string `json:"comment"` 7 | ReadOnly bool `json:"read_only"` 8 | AuthUser string `json:"auth_user"` 9 | Secret string `json:"secret"` 10 | HostsAllow string `json:"hosts_allow"` 11 | } 12 | -------------------------------------------------------------------------------- /internal/apps/s3fs/request.go: -------------------------------------------------------------------------------- 1 | package s3fs 2 | 3 | type Create struct { 4 | Ak string `form:"ak" json:"ak" validate:"required"` 5 | Sk string `form:"sk" json:"sk" validate:"required"` 6 | Bucket string `form:"bucket" json:"bucket" validate:"required"` 7 | URL string `form:"url" json:"url" validate:"required"` 8 | Path string `form:"path" json:"path" validate:"required"` 9 | } 10 | 11 | type Delete struct { 12 | ID int64 `form:"id" json:"id" validate:"required"` 13 | } 14 | -------------------------------------------------------------------------------- /internal/apps/s3fs/types.go: -------------------------------------------------------------------------------- 1 | package s3fs 2 | 3 | type Mount struct { 4 | ID int64 `json:"id"` 5 | Path string `json:"path"` 6 | Bucket string `json:"bucket"` 7 | URL string `json:"url"` 8 | } 9 | -------------------------------------------------------------------------------- /internal/apps/supervisor/request.go: -------------------------------------------------------------------------------- 1 | package supervisor 2 | 3 | type UpdateConfig struct { 4 | Config string `form:"config" json:"config" validate:"required"` 5 | } 6 | 7 | type UpdateProcessConfig struct { 8 | Process string `form:"process" json:"process" validate:"required"` 9 | Config string `form:"config" json:"config" validate:"required"` 10 | } 11 | 12 | type ProcessName struct { 13 | Process string `form:"process" json:"process" validate:"required"` 14 | } 15 | 16 | type CreateProcess struct { 17 | Name string `form:"name" json:"name" validate:"required"` 18 | User string `form:"user" json:"user" validate:"required"` 19 | Path string `form:"path" json:"path" validate:"required"` 20 | Command string `form:"command" json:"command" validate:"required"` 21 | Num int `form:"num" json:"num" validate:"required|min:1"` 22 | } 23 | -------------------------------------------------------------------------------- /internal/apps/supervisor/types.go: -------------------------------------------------------------------------------- 1 | package supervisor 2 | 3 | type Process struct { 4 | Name string `json:"name"` 5 | Status string `json:"status"` 6 | Pid string `json:"pid"` 7 | Uptime string `json:"uptime"` 8 | } 9 | -------------------------------------------------------------------------------- /internal/biz/app.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/tnb-labs/panel/pkg/api" 7 | ) 8 | 9 | type App struct { 10 | ID uint `gorm:"primaryKey" json:"id"` 11 | Slug string `gorm:"not null;default:'';unique" json:"slug"` 12 | Channel string `gorm:"not null;default:''" json:"channel"` 13 | Version string `gorm:"not null;default:''" json:"version"` 14 | Show bool `gorm:"not null;default:false" json:"show"` 15 | ShowOrder int `gorm:"not null;default:0" json:"show_order"` 16 | CreatedAt time.Time `json:"created_at"` 17 | UpdatedAt time.Time `json:"updated_at"` 18 | } 19 | 20 | type AppRepo interface { 21 | All() api.Apps 22 | Get(slug string) (*api.App, error) 23 | UpdateExist(slug string) bool 24 | Installed() ([]*App, error) 25 | GetInstalled(slug string) (*App, error) 26 | GetInstalledAll(query string, cond ...string) ([]*App, error) 27 | GetHomeShow() ([]map[string]string, error) 28 | IsInstalled(query string, cond ...string) (bool, error) 29 | Install(channel, slug string) error 30 | UnInstall(slug string) error 31 | Update(slug string) error 32 | UpdateShow(slug string, show bool) error 33 | } 34 | -------------------------------------------------------------------------------- /internal/biz/backup.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import "github.com/tnb-labs/panel/pkg/types" 4 | 5 | type BackupType string 6 | 7 | const ( 8 | BackupTypePath BackupType = "path" 9 | BackupTypeWebsite BackupType = "website" 10 | BackupTypeMySQL BackupType = "mysql" 11 | BackupTypePostgres BackupType = "postgres" 12 | BackupTypeRedis BackupType = "redis" 13 | BackupTypePanel BackupType = "panel" 14 | ) 15 | 16 | type BackupRepo interface { 17 | List(typ BackupType) ([]*types.BackupFile, error) 18 | Create(typ BackupType, target string, path ...string) error 19 | Delete(typ BackupType, name string) error 20 | Restore(typ BackupType, backup, target string) error 21 | ClearExpired(path, prefix string, save int) error 22 | CutoffLog(path, target string) error 23 | GetPath(typ BackupType) (string, error) 24 | FixPanel() error 25 | UpdatePanel(version, url, checksum string) error 26 | } 27 | -------------------------------------------------------------------------------- /internal/biz/cache.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import "time" 4 | 5 | type CacheKey string 6 | 7 | const ( 8 | CacheKeyApps CacheKey = "apps" 9 | CacheKeyRewrites CacheKey = "rewrites" 10 | ) 11 | 12 | type Cache struct { 13 | Key CacheKey `gorm:"primaryKey" json:"key"` 14 | Value string `gorm:"not null;default:''" json:"value"` 15 | CreatedAt time.Time `json:"created_at"` 16 | UpdatedAt time.Time `json:"updated_at"` 17 | } 18 | 19 | type CacheRepo interface { 20 | Get(key CacheKey, defaultValue ...string) (string, error) 21 | Set(key CacheKey, value string) error 22 | UpdateApps() error 23 | UpdateRewrites() error 24 | } 25 | -------------------------------------------------------------------------------- /internal/biz/cert_account.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/tnb-labs/panel/internal/http/request" 7 | ) 8 | 9 | type CertAccount struct { 10 | ID uint `gorm:"primaryKey" json:"id"` 11 | Email string `gorm:"not null;default:''" json:"email"` 12 | CA string `gorm:"not null;default:'letsencrypt'" json:"ca"` // CA 提供商 (letsencrypt, zerossl, sslcom, google, buypass) 13 | Kid string `gorm:"not null;default:''" json:"kid"` 14 | HmacEncoded string `gorm:"not null;default:''" json:"hmac_encoded"` 15 | PrivateKey string `gorm:"not null;default:''" json:"private_key"` 16 | KeyType string `gorm:"not null;default:'P256'" json:"key_type"` // 密钥类型 (P256, P384, 2048, 3072, 4096) 17 | CreatedAt time.Time `json:"created_at"` 18 | UpdatedAt time.Time `json:"updated_at"` 19 | 20 | Certs []*Cert `gorm:"foreignKey:AccountID" json:"-"` 21 | } 22 | 23 | type CertAccountRepo interface { 24 | List(page, limit uint) ([]*CertAccount, int64, error) 25 | GetDefault(userID uint) (*CertAccount, error) 26 | Get(id uint) (*CertAccount, error) 27 | Create(req *request.CertAccountCreate) (*CertAccount, error) 28 | Update(req *request.CertAccountUpdate) error 29 | Delete(id uint) error 30 | } 31 | -------------------------------------------------------------------------------- /internal/biz/cert_dns.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/tnb-labs/panel/internal/http/request" 7 | "github.com/tnb-labs/panel/pkg/acme" 8 | ) 9 | 10 | type CertDNS struct { 11 | ID uint `gorm:"primaryKey" json:"id"` 12 | Name string `gorm:"not null;default:''" json:"name"` // 备注名称 13 | Type acme.DnsType `gorm:"not null;default:'aliyun'" json:"type"` // DNS 提供商 14 | Data acme.DNSParam `gorm:"not null;serializer:json" json:"dns_param"` 15 | CreatedAt time.Time `json:"created_at"` 16 | UpdatedAt time.Time `json:"updated_at"` 17 | 18 | Certs []*Cert `gorm:"foreignKey:DNSID" json:"-"` 19 | } 20 | 21 | type CertDNSRepo interface { 22 | List(page, limit uint) ([]*CertDNS, int64, error) 23 | Get(id uint) (*CertDNS, error) 24 | Create(req *request.CertDNSCreate) (*CertDNS, error) 25 | Update(req *request.CertDNSUpdate) error 26 | Delete(id uint) error 27 | } 28 | -------------------------------------------------------------------------------- /internal/biz/container.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "github.com/tnb-labs/panel/internal/http/request" 5 | "github.com/tnb-labs/panel/pkg/types" 6 | ) 7 | 8 | type ContainerRepo interface { 9 | ListAll() ([]types.Container, error) 10 | ListByName(name string) ([]types.Container, error) 11 | Create(req *request.ContainerCreate) (string, error) 12 | Remove(id string) error 13 | Start(id string) error 14 | Stop(id string) error 15 | Restart(id string) error 16 | Pause(id string) error 17 | Unpause(id string) error 18 | Kill(id string) error 19 | Rename(id string, newName string) error 20 | Logs(id string) (string, error) 21 | Prune() error 22 | } 23 | -------------------------------------------------------------------------------- /internal/biz/container_compose.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import "github.com/tnb-labs/panel/pkg/types" 4 | 5 | type ContainerComposeRepo interface { 6 | List() ([]types.ContainerCompose, error) 7 | Get(name string) (string, []types.KV, error) 8 | Create(name, compose string, envs []types.KV) error 9 | Update(name, compose string, envs []types.KV) error 10 | Up(name string, force bool) error 11 | Down(name string) error 12 | Remove(name string) error 13 | } 14 | -------------------------------------------------------------------------------- /internal/biz/container_image.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "github.com/tnb-labs/panel/internal/http/request" 5 | "github.com/tnb-labs/panel/pkg/types" 6 | ) 7 | 8 | type ContainerImageRepo interface { 9 | List() ([]types.ContainerImage, error) 10 | Pull(req *request.ContainerImagePull) error 11 | Remove(id string) error 12 | Prune() error 13 | } 14 | -------------------------------------------------------------------------------- /internal/biz/container_network.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "github.com/tnb-labs/panel/internal/http/request" 5 | "github.com/tnb-labs/panel/pkg/types" 6 | ) 7 | 8 | type ContainerNetworkRepo interface { 9 | List() ([]types.ContainerNetwork, error) 10 | Create(req *request.ContainerNetworkCreate) (string, error) 11 | Remove(id string) error 12 | Prune() error 13 | } 14 | -------------------------------------------------------------------------------- /internal/biz/container_volume.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "github.com/tnb-labs/panel/internal/http/request" 5 | "github.com/tnb-labs/panel/pkg/types" 6 | ) 7 | 8 | type ContainerVolumeRepo interface { 9 | List() ([]types.ContainerVolume, error) 10 | Create(req *request.ContainerVolumeCreate) (string, error) 11 | Remove(id string) error 12 | Prune() error 13 | } 14 | -------------------------------------------------------------------------------- /internal/biz/cron.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/tnb-labs/panel/internal/http/request" 7 | ) 8 | 9 | type Cron struct { 10 | ID uint `gorm:"primaryKey" json:"id"` 11 | Name string `gorm:"not null;default:'';unique" json:"name"` 12 | Status bool `gorm:"not null;default:false" json:"status"` 13 | Type string `gorm:"not null;default:''" json:"type"` 14 | Time string `gorm:"not null;default:''" json:"time"` 15 | Shell string `gorm:"not null;default:''" json:"shell"` 16 | Log string `gorm:"not null;default:''" json:"log"` 17 | CreatedAt time.Time `json:"created_at"` 18 | UpdatedAt time.Time `json:"updated_at"` 19 | } 20 | 21 | type CronRepo interface { 22 | Count() (int64, error) 23 | List(page, limit uint) ([]*Cron, int64, error) 24 | Get(id uint) (*Cron, error) 25 | Create(req *request.CronCreate) error 26 | Update(req *request.CronUpdate) error 27 | Delete(id uint) error 28 | Status(id uint, status bool) error 29 | } 30 | -------------------------------------------------------------------------------- /internal/biz/database.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "github.com/tnb-labs/panel/internal/http/request" 5 | ) 6 | 7 | type DatabaseType string 8 | 9 | const ( 10 | DatabaseTypeMysql DatabaseType = "mysql" 11 | DatabaseTypePostgresql DatabaseType = "postgresql" 12 | DatabaseTypeMongoDB DatabaseType = "mongodb" 13 | DatabaseSQLite DatabaseType = "sqlite" 14 | DatabaseTypeRedis DatabaseType = "redis" 15 | ) 16 | 17 | type Database struct { 18 | Type DatabaseType `json:"type"` 19 | Name string `json:"name"` 20 | Server string `json:"server"` 21 | ServerID uint `json:"server_id"` 22 | Encoding string `json:"encoding"` 23 | Comment string `json:"comment"` 24 | } 25 | 26 | type DatabaseRepo interface { 27 | List(page, limit uint) ([]*Database, int64, error) 28 | Create(req *request.DatabaseCreate) error 29 | Delete(serverID uint, name string) error 30 | Comment(req *request.DatabaseComment) error 31 | } 32 | -------------------------------------------------------------------------------- /internal/biz/monitor.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/tnb-labs/panel/internal/http/request" 7 | "github.com/tnb-labs/panel/pkg/types" 8 | ) 9 | 10 | type Monitor struct { 11 | ID uint `gorm:"primaryKey" json:"id"` 12 | Info types.CurrentInfo `gorm:"not null;default:'{}';serializer:json" json:"info"` 13 | CreatedAt time.Time `json:"created_at"` 14 | UpdatedAt time.Time `json:"updated_at"` 15 | } 16 | 17 | type MonitorRepo interface { 18 | GetSetting() (*request.MonitorSetting, error) 19 | UpdateSetting(setting *request.MonitorSetting) error 20 | Clear() error 21 | List(start, end time.Time) ([]*Monitor, error) 22 | } 23 | -------------------------------------------------------------------------------- /internal/biz/safe.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | type SafeRepo interface { 4 | GetSSH() (uint, bool, error) 5 | UpdateSSH(port uint, status bool) error 6 | GetPingStatus() (bool, error) 7 | UpdatePingStatus(status bool) error 8 | } 9 | -------------------------------------------------------------------------------- /internal/biz/task.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import "time" 4 | 5 | type TaskStatus string 6 | 7 | const ( 8 | TaskStatusWaiting TaskStatus = "waiting" 9 | TaskStatusRunning TaskStatus = "running" 10 | TaskStatusSuccess TaskStatus = "finished" 11 | TaskStatusFailed TaskStatus = "failed" 12 | ) 13 | 14 | type Task struct { 15 | ID uint `gorm:"primaryKey" json:"id"` 16 | Name string `gorm:"not null;default:'';index" json:"name"` 17 | Status TaskStatus `gorm:"not null;default:'waiting'" json:"status"` 18 | Shell string `gorm:"not null;default:''" json:"-"` 19 | Log string `gorm:"not null;default:''" json:"log"` 20 | CreatedAt time.Time `json:"created_at"` 21 | UpdatedAt time.Time `json:"updated_at"` 22 | } 23 | 24 | type TaskRepo interface { 25 | HasRunningTask() bool 26 | List(page, limit uint) ([]*Task, int64, error) 27 | Get(id uint) (*Task, error) 28 | Delete(id uint) error 29 | UpdateStatus(id uint, status TaskStatus) error 30 | Push(task *Task) error 31 | } 32 | -------------------------------------------------------------------------------- /internal/biz/user.go: -------------------------------------------------------------------------------- 1 | package biz 2 | 3 | import ( 4 | "image" 5 | "time" 6 | 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type User struct { 11 | ID uint `gorm:"primaryKey" json:"id"` 12 | Username string `gorm:"not null;default:'';unique" json:"username"` 13 | Password string `gorm:"not null;default:''" json:"password"` 14 | Email string `gorm:"not null;default:''" json:"email"` 15 | TwoFA string `gorm:"not null;default:''" json:"two_fa"` // 2FA secret,为空表示未开启 16 | CreatedAt time.Time `json:"created_at"` 17 | UpdatedAt time.Time `json:"updated_at"` 18 | DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` 19 | 20 | Tokens []*UserToken `gorm:"foreignKey:UserID" json:"-"` 21 | } 22 | 23 | type UserRepo interface { 24 | List(page, limit uint) ([]*User, int64, error) 25 | Get(id uint) (*User, error) 26 | Create(username, password, email string) (*User, error) 27 | UpdatePassword(id uint, password string) error 28 | UpdateEmail(id uint, email string) error 29 | Delete(id uint) error 30 | CheckPassword(username, password string) (*User, error) 31 | IsTwoFA(username string) (bool, error) 32 | GenerateTwoFA(id uint) (image.Image, string, string, error) 33 | UpdateTwoFA(id uint, code, secret string) error 34 | } 35 | -------------------------------------------------------------------------------- /internal/bootstrap/bootstrap.go: -------------------------------------------------------------------------------- 1 | package bootstrap 2 | 3 | import "github.com/google/wire" 4 | 5 | // ProviderSet is bootstrap providers. 6 | var ProviderSet = wire.NewSet(NewConf, NewT, NewLog, NewCli, NewValidator, NewRouter, NewHttp, NewDB, NewMigrate, NewLoader, NewSession, NewCron, NewQueue) 7 | -------------------------------------------------------------------------------- /internal/bootstrap/conf.go: -------------------------------------------------------------------------------- 1 | package bootstrap 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/knadh/koanf/parsers/yaml" 8 | "github.com/knadh/koanf/providers/file" 9 | "github.com/knadh/koanf/v2" 10 | 11 | "github.com/tnb-labs/panel/internal/app" 12 | "github.com/tnb-labs/panel/pkg/io" 13 | ) 14 | 15 | func NewConf() (*koanf.Koanf, error) { 16 | config := "/usr/local/etc/panel/config.yml" 17 | if !io.Exists(config) { 18 | config = "config.yml" 19 | } 20 | 21 | conf := koanf.New(".") 22 | if err := conf.Load(file.Provider(config), yaml.Parser()); err != nil { 23 | return nil, err 24 | } 25 | 26 | initGlobal(conf) 27 | return conf, nil 28 | } 29 | 30 | func initGlobal(conf *koanf.Koanf) { 31 | app.Key = conf.MustString("app.key") 32 | if len(app.Key) != 32 { 33 | log.Fatalf("panel app key must be 32 characters") 34 | } 35 | 36 | app.Root = conf.MustString("app.root") 37 | app.Locale = conf.MustString("app.locale") 38 | 39 | // 初始化时区 40 | loc, err := time.LoadLocation(conf.MustString("app.timezone")) 41 | if err != nil { 42 | log.Fatalf("failed to load timezone: %v", err) 43 | } 44 | time.Local = loc 45 | } 46 | -------------------------------------------------------------------------------- /internal/bootstrap/cron.go: -------------------------------------------------------------------------------- 1 | package bootstrap 2 | 3 | import ( 4 | "log/slog" 5 | 6 | "github.com/knadh/koanf/v2" 7 | "github.com/robfig/cron/v3" 8 | 9 | "github.com/tnb-labs/panel/internal/job" 10 | pkgcron "github.com/tnb-labs/panel/pkg/cron" 11 | ) 12 | 13 | func NewCron(conf *koanf.Koanf, log *slog.Logger, jobs *job.Jobs) (*cron.Cron, error) { 14 | logger := pkgcron.NewLogger(log, conf.Bool("app.debug")) 15 | 16 | c := cron.New( 17 | cron.WithParser(cron.NewParser( 18 | cron.SecondOptional|cron.Minute|cron.Hour|cron.Dom|cron.Month|cron.Dow|cron.Descriptor, 19 | )), 20 | cron.WithLogger(logger), 21 | cron.WithChain(cron.Recover(logger), cron.SkipIfStillRunning(logger)), 22 | ) 23 | if err := jobs.Register(c); err != nil { 24 | return nil, err 25 | } 26 | 27 | return c, nil 28 | } 29 | -------------------------------------------------------------------------------- /internal/bootstrap/db.go: -------------------------------------------------------------------------------- 1 | package bootstrap 2 | 3 | import ( 4 | "log/slog" 5 | "path/filepath" 6 | 7 | "github.com/go-gormigrate/gormigrate/v2" 8 | "github.com/knadh/koanf/v2" 9 | _ "github.com/ncruces/go-sqlite3/embed" 10 | "github.com/ncruces/go-sqlite3/gormlite" 11 | sloggorm "github.com/orandin/slog-gorm" 12 | "gorm.io/gorm" 13 | 14 | "github.com/tnb-labs/panel/internal/app" 15 | "github.com/tnb-labs/panel/internal/migration" 16 | ) 17 | 18 | func NewDB(conf *koanf.Koanf, log *slog.Logger) (*gorm.DB, error) { 19 | // You can use any other database, like MySQL or PostgreSQL. 20 | return gorm.Open(gormlite.Open(filepath.Join(app.Root, "panel/storage/app.db")), &gorm.Config{ 21 | Logger: sloggorm.New(sloggorm.WithHandler(log.Handler())), 22 | SkipDefaultTransaction: true, 23 | DisableForeignKeyConstraintWhenMigrating: true, 24 | }) 25 | } 26 | 27 | func NewMigrate(db *gorm.DB) *gormigrate.Gormigrate { 28 | return gormigrate.New(db, &gormigrate.Options{ 29 | UseTransaction: true, // Note: MySQL not support DDL transaction 30 | }, migration.Migrations) 31 | } 32 | -------------------------------------------------------------------------------- /internal/bootstrap/http.go: -------------------------------------------------------------------------------- 1 | package bootstrap 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/bddjr/hlfhr" 9 | "github.com/go-chi/chi/v5" 10 | "github.com/knadh/koanf/v2" 11 | "github.com/leonelquinteros/gotext" 12 | 13 | "github.com/tnb-labs/panel/internal/http/middleware" 14 | "github.com/tnb-labs/panel/internal/route" 15 | ) 16 | 17 | func NewRouter(t *gotext.Locale, middlewares *middleware.Middlewares, http *route.Http, ws *route.Ws) (*chi.Mux, error) { 18 | r := chi.NewRouter() 19 | 20 | // add middleware 21 | r.Use(middlewares.Globals(t, r)...) 22 | // add http route 23 | http.Register(r) 24 | // add ws route 25 | ws.Register(r) 26 | 27 | return r, nil 28 | } 29 | 30 | func NewHttp(conf *koanf.Koanf, r *chi.Mux) (*hlfhr.Server, error) { 31 | srv := hlfhr.New(&http.Server{ 32 | Addr: fmt.Sprintf(":%d", conf.MustInt("http.port")), 33 | Handler: http.AllowQuerySemicolons(r), 34 | MaxHeaderBytes: 2048 << 20, 35 | }) 36 | srv.HttpOnHttpsPortErrorHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 37 | hlfhr.RedirectToHttps(w, r, http.StatusTemporaryRedirect) 38 | }) 39 | 40 | if conf.Bool("http.tls") { 41 | srv.TLSConfig = &tls.Config{ 42 | MinVersion: tls.VersionTLS12, 43 | } 44 | } 45 | 46 | return srv, nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/bootstrap/logger.go: -------------------------------------------------------------------------------- 1 | package bootstrap 2 | 3 | import ( 4 | "log/slog" 5 | "path/filepath" 6 | 7 | "github.com/knadh/koanf/v2" 8 | "gopkg.in/natefinch/lumberjack.v2" 9 | 10 | "github.com/tnb-labs/panel/internal/app" 11 | ) 12 | 13 | func NewLog(conf *koanf.Koanf) *slog.Logger { 14 | ljLogger := &lumberjack.Logger{ 15 | Filename: filepath.Join(app.Root, "panel/storage/logs/app.log"), 16 | MaxSize: 10, 17 | MaxAge: 30, 18 | Compress: true, 19 | } 20 | 21 | level := slog.LevelInfo 22 | if conf.Bool("app.debug") { 23 | level = slog.LevelDebug 24 | } 25 | 26 | log := slog.New(slog.NewJSONHandler(ljLogger, &slog.HandlerOptions{ 27 | Level: level, 28 | })) 29 | slog.SetDefault(log) 30 | 31 | return log 32 | } 33 | -------------------------------------------------------------------------------- /internal/bootstrap/queue.go: -------------------------------------------------------------------------------- 1 | package bootstrap 2 | 3 | import ( 4 | "github.com/tnb-labs/panel/pkg/queue" 5 | ) 6 | 7 | func NewQueue() *queue.Queue { 8 | return queue.New(100) 9 | } 10 | -------------------------------------------------------------------------------- /internal/bootstrap/session.go: -------------------------------------------------------------------------------- 1 | package bootstrap 2 | 3 | import ( 4 | "github.com/go-rat/gormstore" 5 | "github.com/go-rat/sessions" 6 | "github.com/knadh/koanf/v2" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | func NewSession(conf *koanf.Koanf, db *gorm.DB) (*sessions.Manager, error) { 11 | // initialize session manager 12 | lifetime := conf.Int("session.lifetime") 13 | // TODO: will remove this fallback in v3 14 | if lifetime == 0 { 15 | lifetime = 120 16 | } 17 | manager, err := sessions.NewManager(&sessions.ManagerOptions{ 18 | Key: conf.MustString("app.key"), 19 | Lifetime: lifetime, 20 | GcInterval: 5, 21 | DisableDefaultDriver: true, 22 | }) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | // extend gorm store driver 28 | store := gormstore.New(db) 29 | if err = manager.Extend("default", store); err != nil { 30 | return nil, err 31 | } 32 | 33 | return manager, nil 34 | } 35 | -------------------------------------------------------------------------------- /internal/bootstrap/t.go: -------------------------------------------------------------------------------- 1 | package bootstrap 2 | 3 | import ( 4 | "github.com/knadh/koanf/v2" 5 | "github.com/leonelquinteros/gotext" 6 | 7 | "github.com/tnb-labs/panel/pkg/embed" 8 | ) 9 | 10 | func NewT(conf *koanf.Koanf) (*gotext.Locale, error) { 11 | locale := conf.String("app.locale") 12 | l := gotext.NewLocaleFSWithPath(locale, embed.LocalesFS, "locales") 13 | l.AddDomain("backend") 14 | 15 | return l, nil 16 | } 17 | -------------------------------------------------------------------------------- /internal/bootstrap/validator.go: -------------------------------------------------------------------------------- 1 | package bootstrap 2 | 3 | import ( 4 | "github.com/gookit/validate" 5 | "github.com/gookit/validate/locales/ruru" 6 | "github.com/gookit/validate/locales/zhcn" 7 | "github.com/gookit/validate/locales/zhtw" 8 | "github.com/knadh/koanf/v2" 9 | "gorm.io/gorm" 10 | 11 | "github.com/tnb-labs/panel/internal/http/rule" 12 | ) 13 | 14 | // NewValidator just for register global rules 15 | func NewValidator(conf *koanf.Koanf, db *gorm.DB) *validate.Validation { 16 | if conf.String("app.locale") == "zh_CN" { 17 | zhcn.RegisterGlobal() 18 | } else if conf.String("app.locale") == "zh_TW" { 19 | zhtw.RegisterGlobal() 20 | } else if conf.String("app.locale") == "ru_RU" { 21 | ruru.RegisterGlobal() 22 | } 23 | validate.Config(func(opt *validate.GlobalOption) { 24 | opt.StopOnError = false 25 | opt.SkipOnEmpty = true 26 | }) 27 | 28 | // register global rules 29 | rule.GlobalRules(db) 30 | 31 | return validate.NewEmpty() 32 | } 33 | -------------------------------------------------------------------------------- /internal/data/data.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import "github.com/google/wire" 4 | 5 | // ProviderSet is data providers. 6 | var ProviderSet = wire.NewSet( 7 | NewAppRepo, 8 | NewBackupRepo, 9 | NewCacheRepo, 10 | NewCertRepo, 11 | NewCertAccountRepo, 12 | NewCertDNSRepo, 13 | NewContainerRepo, 14 | NewContainerComposeRepo, 15 | NewContainerImageRepo, 16 | NewContainerNetworkRepo, 17 | NewContainerVolumeRepo, 18 | NewCronRepo, 19 | NewDatabaseRepo, 20 | NewDatabaseServerRepo, 21 | NewDatabaseUserRepo, 22 | NewMonitorRepo, 23 | NewSafeRepo, 24 | NewSettingRepo, 25 | NewSSHRepo, 26 | NewTaskRepo, 27 | NewUserRepo, 28 | NewUserTokenRepo, 29 | NewWebsiteRepo, 30 | ) 31 | -------------------------------------------------------------------------------- /internal/data/helper.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/go-resty/resty/v2" 10 | ) 11 | 12 | func getDockerClient(sock string) *resty.Client { 13 | client := resty.New() 14 | client.SetTimeout(1 * time.Minute) 15 | client.SetRetryCount(2) 16 | client.SetTransport(&http.Transport{ 17 | DialContext: func(ctx context.Context, _ string, _ string) (net.Conn, error) { 18 | return (&net.Dialer{}).DialContext(ctx, "unix", sock) 19 | }, 20 | }) 21 | client.SetBaseURL("http://d/v1.40") 22 | return client 23 | } 24 | -------------------------------------------------------------------------------- /internal/http/middleware/helper.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/go-rat/chix" 8 | ) 9 | 10 | func Abort(w http.ResponseWriter, code int, format string, args ...any) { 11 | render := chix.NewRender(w) 12 | defer render.Release() 13 | render.Header(chix.HeaderContentType, chix.MIMEApplicationJSONCharsetUTF8) // must before Status() 14 | render.Status(code) 15 | if len(args) > 0 { 16 | format = fmt.Sprintf(format, args...) 17 | } 18 | render.JSON(chix.M{ 19 | "msg": format, 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /internal/http/middleware/status.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/leonelquinteros/gotext" 7 | 8 | "github.com/tnb-labs/panel/internal/app" 9 | ) 10 | 11 | // Status 检查程序状态 12 | func Status(t *gotext.Locale) func(next http.Handler) http.Handler { 13 | return func(next http.Handler) http.Handler { 14 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 | switch app.Status { 16 | case app.StatusUpgrade: 17 | Abort(w, http.StatusServiceUnavailable, t.Get("panel is upgrading, please refresh later")) 18 | return 19 | case app.StatusMaintain: 20 | Abort(w, http.StatusServiceUnavailable, t.Get("panel is maintaining, please refresh later")) 21 | return 22 | case app.StatusClosed: 23 | Abort(w, http.StatusServiceUnavailable, t.Get("panel is closed")) 24 | return 25 | case app.StatusFailed: 26 | Abort(w, http.StatusInternalServerError, t.Get("panel run error, please check or contact support")) 27 | return 28 | default: 29 | next.ServeHTTP(w, r) 30 | } 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /internal/http/middleware/throttle.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/sethvargo/go-limiter/httplimit" 9 | "github.com/sethvargo/go-limiter/memorystore" 10 | ) 11 | 12 | // Throttle 限流器 13 | func Throttle(tokens uint64, interval time.Duration) func(next http.Handler) http.Handler { 14 | store, err := memorystore.New(&memorystore.Config{ 15 | Tokens: tokens, 16 | Interval: interval, 17 | }) 18 | if err != nil { 19 | log.Fatalf("failed to create throttle memorystore: %v", err) 20 | } 21 | 22 | limiter, err := httplimit.NewMiddleware(store, httplimit.IPKeyFunc()) 23 | if err != nil { 24 | log.Fatalf("failed to initialize throttle middleware: %v", err) 25 | } 26 | 27 | return limiter.Handle 28 | } 29 | -------------------------------------------------------------------------------- /internal/http/request/app.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type App struct { 4 | Slug string `json:"slug" form:"slug" validate:"required|notExists:apps,slug"` 5 | Channel string `json:"channel" form:"channel" validate:"required"` 6 | } 7 | 8 | type AppSlug struct { 9 | Slug string `json:"slug" form:"slug" validate:"required"` 10 | } 11 | 12 | type AppUpdateShow struct { 13 | Slug string `json:"slug" form:"slug" validate:"required|exists:apps,slug"` 14 | Show bool `json:"show" form:"show"` 15 | } 16 | -------------------------------------------------------------------------------- /internal/http/request/backup.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import "mime/multipart" 4 | 5 | type BackupList struct { 6 | Type string `uri:"type" form:"type" validate:"required|in:path,website,mysql,postgres,redis,panel"` 7 | } 8 | 9 | type BackupCreate struct { 10 | Type string `uri:"type" form:"type" validate:"required|in:website,mysql,postgres,redis,panel"` 11 | Target string `json:"target" form:"target" validate:"required"` 12 | Path string `json:"path" form:"path"` 13 | } 14 | 15 | type BackupUpload struct { 16 | Type string `uri:"type" form:"type"` // 校验没有必要,因为根本没经过验证器 17 | File *multipart.FileHeader `form:"file"` 18 | } 19 | 20 | type BackupFile struct { 21 | Type string `uri:"type" form:"type" validate:"required|in:website,mysql,postgres,redis,panel"` 22 | File string `json:"file" form:"file" validate:"required"` 23 | } 24 | 25 | type BackupRestore struct { 26 | Type string `uri:"type" form:"type" validate:"required|in:website,mysql,postgres,redis,panel"` 27 | File string `json:"file" form:"file" validate:"required"` 28 | Target string `json:"target" form:"target" validate:"required"` 29 | } 30 | -------------------------------------------------------------------------------- /internal/http/request/cert_account.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type CertAccountCreate struct { 4 | CA string `form:"ca" json:"ca" validate:"required|in:googlecn,google,letsencrypt,buypass,zerossl,sslcom"` 5 | Email string `form:"email" json:"email" validate:"required"` 6 | Kid string `form:"kid" json:"kid"` 7 | HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"` 8 | KeyType string `form:"key_type" json:"key_type" validate:"required|in:P256,P384,2048,3072,4096"` 9 | } 10 | 11 | type CertAccountUpdate struct { 12 | ID uint `form:"id" json:"id" validate:"required|exists:cert_accounts,id"` 13 | CA string `form:"ca" json:"ca" validate:"required|in:googlecn,google,letsencrypt,buypass,zerossl,sslcom"` 14 | Email string `form:"email" json:"email" validate:"required"` 15 | Kid string `form:"kid" json:"kid"` 16 | HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"` 17 | KeyType string `form:"key_type" json:"key_type" validate:"required|in:P256,P384,2048,3072,4096"` 18 | } 19 | -------------------------------------------------------------------------------- /internal/http/request/cert_dns.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import "github.com/tnb-labs/panel/pkg/acme" 4 | 5 | type CertDNSCreate struct { 6 | Type acme.DnsType `form:"type" json:"type" validate:"required|in:aliyun,tencent,huawei,westcn,cloudflare,godaddy,gcore,porkbun,namecheap,namesilo,namecom,cloudns,duckdns,hetzner,linode,vercel"` 7 | Name string `form:"name" json:"name" validate:"required"` 8 | Data acme.DNSParam `form:"data" json:"data" validate:"required"` 9 | } 10 | 11 | type CertDNSUpdate struct { 12 | ID uint `form:"id" json:"id" validate:"required|exists:cert_dns,id"` 13 | Type acme.DnsType `form:"type" json:"type" validate:"required|in:aliyun,tencent,huawei,westcn,cloudflare,godaddy,gcore,porkbun,namecheap,namesilo,namecom,cloudns,duckdns,hetzner,linode,vercel"` 14 | Name string `form:"name" json:"name" validate:"required"` 15 | Data acme.DNSParam `form:"data" json:"data" validate:"required"` 16 | } 17 | -------------------------------------------------------------------------------- /internal/http/request/common.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type ID struct { 4 | ID uint `json:"id" form:"id" query:"id" uri:"id" validate:"required|min:1"` 5 | } 6 | -------------------------------------------------------------------------------- /internal/http/request/container_compose.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import "github.com/tnb-labs/panel/pkg/types" 4 | 5 | type ContainerComposeGet struct { 6 | Name string `uri:"name" validate:"required"` 7 | } 8 | 9 | type ContainerComposeCreate struct { 10 | Name string `json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"` 11 | Compose string `json:"compose" validate:"required"` 12 | Envs []types.KV `json:"envs"` 13 | } 14 | 15 | type ContainerComposeUpdate struct { 16 | Name string `uri:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"` 17 | Compose string `json:"compose" validate:"required"` 18 | Envs []types.KV `json:"envs"` 19 | } 20 | 21 | type ContainerComposeUp struct { 22 | Name string `uri:"name" validate:"required"` 23 | Force bool `json:"force"` 24 | } 25 | 26 | type ContainerComposeDown struct { 27 | Name string `uri:"name" validate:"required"` 28 | } 29 | 30 | type ContainerComposeRemove struct { 31 | Name string `uri:"name" validate:"required"` 32 | } 33 | -------------------------------------------------------------------------------- /internal/http/request/container_image.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type ContainerImageID struct { 4 | ID string `json:"id" form:"id"` 5 | } 6 | 7 | type ContainerImagePull struct { 8 | Name string `form:"name" json:"name" validate:"required"` 9 | Auth bool `form:"auth" json:"auth"` 10 | Username string `form:"username" json:"username" validate:"requiredIf:Auth,true"` 11 | Password string `form:"password" json:"password" validate:"requiredIf:Auth,true"` 12 | } 13 | -------------------------------------------------------------------------------- /internal/http/request/container_network.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import "github.com/tnb-labs/panel/pkg/types" 4 | 5 | type ContainerNetworkID struct { 6 | ID string `json:"id" form:"id" validate:"required"` 7 | } 8 | 9 | type ContainerNetworkCreate struct { 10 | Name string `form:"name" json:"name" validate:"required"` 11 | Driver string `form:"driver" json:"driver" validate:"required|in:bridge,host,overlay,macvlan,ipvlan,none"` 12 | Ipv4 types.ContainerContainerNetwork `form:"ipv4" json:"ipv4"` 13 | Ipv6 types.ContainerContainerNetwork `form:"ipv6" json:"ipv6"` 14 | Labels []types.KV `form:"labels" json:"labels"` 15 | Options []types.KV `form:"options" json:"options"` 16 | } 17 | -------------------------------------------------------------------------------- /internal/http/request/container_volume.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import "github.com/tnb-labs/panel/pkg/types" 4 | 5 | type ContainerVolumeID struct { 6 | ID string `json:"id" form:"id" validate:"required"` 7 | } 8 | 9 | type ContainerVolumeCreate struct { 10 | Name string `form:"name" json:"name" validate:"required"` 11 | Driver string `form:"driver" json:"driver" validate:"required|in:local"` 12 | Labels []types.KV `form:"labels" json:"labels"` 13 | Options []types.KV `form:"options" json:"options"` 14 | } 15 | -------------------------------------------------------------------------------- /internal/http/request/cron.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type CronCreate struct { 4 | Name string `form:"name" json:"name" validate:"required|notExists:crons,name"` 5 | Type string `form:"type" json:"type" validate:"required"` 6 | Time string `form:"time" json:"time" validate:"required|cron"` 7 | Script string `form:"script" json:"script"` 8 | BackupType string `form:"backup_type" json:"backup_type" validate:"requiredIf:Type,backup"` 9 | BackupPath string `form:"backup_path" json:"backup_path"` 10 | Target string `form:"target" json:"target" validate:"requiredIf:Type,backup,cutoff"` 11 | Save int `form:"save" json:"save" validate:"required"` 12 | } 13 | 14 | type CronUpdate struct { 15 | ID uint `form:"id" json:"id" validate:"required|exists:crons,id"` 16 | Name string `form:"name" json:"name" validate:"required"` 17 | Time string `form:"time" json:"time" validate:"required|cron"` 18 | Script string `form:"script" json:"script" validate:"required"` 19 | } 20 | 21 | type CronStatus struct { 22 | ID uint `form:"id" json:"id" validate:"required|exists:crons,id"` 23 | Status bool `form:"status" json:"status"` 24 | } 25 | -------------------------------------------------------------------------------- /internal/http/request/dashboard.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type DashboardCurrent struct { 4 | Nets []string `json:"nets" form:"nets"` 5 | Disks []string `json:"disks" form:"disks"` 6 | } 7 | -------------------------------------------------------------------------------- /internal/http/request/database.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type DatabaseCreate struct { 4 | ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"` 5 | Name string `form:"name" json:"name" validate:"required"` 6 | CreateUser bool `form:"create_user" json:"create_user"` 7 | Username string `form:"username" json:"username" validate:"requiredIf:CreateUser,true"` 8 | Password string `form:"password" json:"password" validate:"requiredIf:CreateUser,true"` 9 | Host string `form:"host" json:"host"` 10 | Comment string `form:"comment" json:"comment"` 11 | } 12 | 13 | type DatabaseDelete struct { 14 | ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"` 15 | Name string `form:"name" json:"name" validate:"required"` 16 | } 17 | 18 | type DatabaseComment struct { 19 | ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"` 20 | Name string `form:"name" json:"name" validate:"required"` 21 | Comment string `form:"comment" json:"comment"` 22 | } 23 | -------------------------------------------------------------------------------- /internal/http/request/database_server.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type DatabaseServerCreate struct { 4 | Name string `form:"name" json:"name" validate:"required|notExists:database_servers,name"` 5 | Type string `form:"type" json:"type" validate:"required|in:mysql,postgresql,redis"` 6 | Host string `form:"host" json:"host" validate:"required"` 7 | Port uint `form:"port" json:"port" validate:"required|min:1|max:65535"` 8 | Username string `form:"username" json:"username"` 9 | Password string `form:"password" json:"password"` 10 | Remark string `form:"remark" json:"remark"` 11 | } 12 | 13 | type DatabaseServerUpdate struct { 14 | ID uint `form:"id" json:"id" validate:"required|exists:database_servers,id"` 15 | Name string `form:"name" json:"name" validate:"required"` 16 | Host string `form:"host" json:"host" validate:"required"` 17 | Port uint `form:"port" json:"port" validate:"required|min:1|max:65535"` 18 | Username string `form:"username" json:"username"` 19 | Password string `form:"password" json:"password"` 20 | Remark string `form:"remark" json:"remark"` 21 | } 22 | 23 | type DatabaseServerUpdateRemark struct { 24 | ID uint `form:"id" json:"id" validate:"required|exists:database_servers,id"` 25 | Remark string `form:"remark" json:"remark"` 26 | } 27 | -------------------------------------------------------------------------------- /internal/http/request/database_user.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type DatabaseUserCreate struct { 4 | ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"` 5 | Username string `form:"username" json:"username" validate:"required"` 6 | Password string `form:"password" json:"password" validate:"required"` 7 | Host string `form:"host" json:"host"` 8 | Privileges []string `form:"privileges" json:"privileges"` 9 | Remark string `form:"remark" json:"remark"` 10 | } 11 | 12 | type DatabaseUserUpdate struct { 13 | ID uint `form:"id" json:"id" validate:"required|exists:database_users,id"` 14 | Password string `form:"password" json:"password"` 15 | Privileges []string `form:"privileges" json:"privileges"` 16 | Remark string `form:"remark" json:"remark"` 17 | } 18 | 19 | type DatabaseUserUpdateRemark struct { 20 | ID uint `form:"id" json:"id" validate:"required|exists:database_users,id"` 21 | Remark string `form:"remark" json:"remark"` 22 | } 23 | -------------------------------------------------------------------------------- /internal/http/request/monitor.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type MonitorSetting struct { 4 | Enabled bool `json:"enabled"` 5 | Days uint `json:"days"` 6 | } 7 | 8 | type MonitorList struct { 9 | Start int64 `json:"start"` 10 | End int64 `json:"end"` 11 | } 12 | -------------------------------------------------------------------------------- /internal/http/request/paginate.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type Paginate struct { 8 | Page uint `json:"page" form:"page" query:"page" validate:"required|min:1"` 9 | Limit uint `json:"limit" form:"limit" query:"limit" validate:"required|min:1|max:10000"` 10 | } 11 | 12 | func (r *Paginate) Messages(_ *http.Request) map[string]string { 13 | return map[string]string{ 14 | "Page.gte": "页码必须大于或等于1", 15 | "Limit.gte": "每页数量必须大于或等于1", 16 | "Limit.lte": "每页数量必须小于或等于10000", 17 | "Page.number": "页码必须是数字", 18 | "Limit.number": "每页数量必须是数字", 19 | "Page.required": "页码不能为空", 20 | "Limit.required": "每页数量不能为空", 21 | } 22 | } 23 | 24 | func (r *Paginate) Prepare(_ *http.Request) error { 25 | if r.Page == 0 { 26 | r.Page = 1 27 | } 28 | if r.Limit == 0 { 29 | r.Limit = 10 30 | } 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /internal/http/request/process.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type ProcessKill struct { 4 | PID int32 `json:"pid" validate:"required"` 5 | } 6 | -------------------------------------------------------------------------------- /internal/http/request/request.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type WithAuthorize interface { 8 | Authorize(r *http.Request) error 9 | } 10 | 11 | type WithPrepare interface { 12 | Prepare(r *http.Request) error 13 | } 14 | 15 | type WithRules interface { 16 | Rules(r *http.Request) map[string]string 17 | } 18 | 19 | type WithFilters interface { 20 | Filters(r *http.Request) map[string]string 21 | } 22 | 23 | type WithMessages interface { 24 | Messages(r *http.Request) map[string]string 25 | } 26 | -------------------------------------------------------------------------------- /internal/http/request/safe.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type SafeUpdateSSH struct { 4 | Port uint `json:"port" form:"port" validate:"required|min:1|max:65535"` 5 | Status bool `json:"status" form:"status"` 6 | } 7 | 8 | type SafeUpdatePingStatus struct { 9 | Status bool `json:"status" form:"status"` 10 | } 11 | -------------------------------------------------------------------------------- /internal/http/request/systemctl.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type SystemctlService struct { 4 | Service string `json:"service" validate:"required"` 5 | } 6 | -------------------------------------------------------------------------------- /internal/http/request/toolbox_benchmark.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type ToolboxBenchmarkTest struct { 4 | Name string `json:"name" validate:"required|in:image,machine,compile,encryption,compression,physics,json,memory,disk"` 5 | } 6 | -------------------------------------------------------------------------------- /internal/http/request/toolbox_system.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import "time" 4 | 5 | type ToolboxSystemDNS struct { 6 | DNS1 string `form:"dns1" json:"dns1" validate:"required"` 7 | DNS2 string `form:"dns2" json:"dns2" validate:"required"` 8 | } 9 | 10 | type ToolboxSystemSWAP struct { 11 | Size int64 `form:"size" json:"size" validate:"min:0"` 12 | } 13 | 14 | type ToolboxSystemTimezone struct { 15 | Timezone string `form:"timezone" json:"timezone" validate:"required"` 16 | } 17 | 18 | type ToolboxSystemTime struct { 19 | Time time.Time `form:"time" json:"time" validate:"required"` 20 | } 21 | 22 | type ToolboxSystemHostname struct { 23 | Hostname string `form:"hostname" json:"hostname" validate:"required|regex:^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$"` 24 | } 25 | 26 | type ToolboxSystemHosts struct { 27 | Hosts string `form:"hosts" json:"hosts"` 28 | } 29 | 30 | type ToolboxSystemPassword struct { 31 | Password string `form:"password" json:"password" validate:"required|password"` 32 | } 33 | -------------------------------------------------------------------------------- /internal/http/request/user.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | type UserID struct { 4 | ID uint `json:"id" validate:"required|exists:users,id"` 5 | } 6 | 7 | type UserLogin struct { 8 | Username string `json:"username" validate:"required"` 9 | Password string `json:"password" validate:"required"` 10 | SafeLogin bool `json:"safe_login"` 11 | PassCode string `json:"pass_code"` 12 | } 13 | 14 | type UserIsTwoFA struct { 15 | Username string `query:"username" validate:"required"` 16 | } 17 | 18 | type UserCreate struct { 19 | Username string `json:"username" validate:"required|notExists:users,username"` 20 | Password string `json:"password" validate:"required|password"` 21 | Email string `json:"email" validate:"required|email"` 22 | } 23 | 24 | type UserUpdatePassword struct { 25 | ID uint `json:"id" validate:"required|exists:users,id"` 26 | Password string `json:"password" validate:"required|password"` 27 | } 28 | 29 | type UserUpdateEmail struct { 30 | ID uint `json:"id" validate:"required|exists:users,id"` 31 | Email string `json:"email" validate:"required|email"` 32 | } 33 | 34 | type UserUpdateTwoFA struct { 35 | ID uint `uri:"id" validate:"required|exists:users,id"` 36 | Secret string `json:"secret"` 37 | Code string `json:"code"` 38 | } 39 | -------------------------------------------------------------------------------- /internal/http/request/user_token.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import "net/http" 4 | 5 | type UserTokenList struct { 6 | UserID uint `query:"user_id"` 7 | Paginate 8 | } 9 | 10 | type UserTokenCreate struct { 11 | UserID uint `json:"user_id" validate:"required|exists:users,id"` 12 | IPs []string `json:"ips"` 13 | ExpiredAt int64 `json:"expired_at" validate:"required"` 14 | } 15 | 16 | func (r *UserTokenCreate) Rules(_ *http.Request) map[string]string { 17 | return map[string]string{ 18 | "IPs.*": "required|ipcidr", 19 | } 20 | } 21 | 22 | type UserTokenUpdate struct { 23 | ID uint `uri:"id"` 24 | IPs []string `json:"ips"` 25 | ExpiredAt int64 `json:"expired_at" validate:"required"` 26 | } 27 | 28 | func (r *UserTokenUpdate) Rules(_ *http.Request) map[string]string { 29 | return map[string]string{ 30 | "IPs.*": "required|ipcidr", 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/http/rule/cron.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/spf13/cast" 7 | ) 8 | 9 | // Cron 校验规则 10 | type Cron struct { 11 | re *regexp.Regexp 12 | } 13 | 14 | func NewCron() *Cron { 15 | return &Cron{ 16 | re: regexp.MustCompile(`(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(ns|us|µs|ms|s|m|h))+)|((((\d+,)+\d+|((\*|\d+)(\/|-)\d+)|\d+|\*) ?){5,7})`), 17 | } 18 | } 19 | 20 | func (s *Cron) Passes(val any, options ...any) bool { 21 | return s.re.MatchString(cast.ToString(val)) 22 | } 23 | -------------------------------------------------------------------------------- /internal/http/rule/exists.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | // Exists 验证一个值在某个表中的字段中存在,支持同时判断多个字段 10 | // Exists verify a value exists in a table field, support judging multiple fields at the same time 11 | // 用法:exists:表名称,字段名称,字段名称,字段名称 12 | // Usage: exists:table_name,field_name,field_name,field_name 13 | // 例子:exists:users,phone,email 14 | // Example: exists:users,phone,email 15 | type Exists struct { 16 | db *gorm.DB 17 | } 18 | 19 | func NewExists(db *gorm.DB) *Exists { 20 | return &Exists{db: db} 21 | } 22 | 23 | func (r *Exists) Passes(val any, options ...any) bool { 24 | if len(options) < 2 { 25 | return false 26 | } 27 | 28 | tableName := options[0].(string) 29 | fieldNames := options[1:] 30 | 31 | query := r.db.Table(tableName).Where(fmt.Sprintf("%s = ?", fieldNames[0]), val) 32 | for _, fieldName := range fieldNames[1:] { 33 | query = query.Or(fmt.Sprintf("%s = ?", fieldName), val) 34 | } 35 | 36 | var count int64 37 | err := query.Count(&count).Error 38 | if err != nil { 39 | return false 40 | } 41 | 42 | return count != 0 43 | } 44 | -------------------------------------------------------------------------------- /internal/http/rule/ip_cidr.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import "net" 4 | 5 | // IPCIDR 验证一个值是否是一个有效的 IP 或 CIDR 格式 6 | type IPCIDR struct{} 7 | 8 | func NewIPCIDR() *IPCIDR { 9 | return &IPCIDR{} 10 | } 11 | 12 | func (r *IPCIDR) Passes(val any, options ...any) bool { 13 | if str, ok := val.(string); ok { 14 | if ip := net.ParseIP(str); ip != nil { 15 | return true // 是有效的 IP 16 | } 17 | if _, _, err := net.ParseCIDR(str); err == nil { 18 | return true // 是有效的 CIDR 19 | } 20 | } 21 | return false // 既不是 IP 也不是 CIDR 22 | } 23 | -------------------------------------------------------------------------------- /internal/http/rule/not_exists.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | // NotExists 验证一个值在某个表中的字段中不存在,支持同时判断多个字段 10 | // NotExists verify a value does not exist in a table field, support judging multiple fields at the same time 11 | // 用法:notExists:表名称,字段名称,字段名称,字段名称 12 | // Usage: notExists:table_name,field_name,field_name,field_name 13 | // 例子:notExists:users,phone,email 14 | // Example: notExists:users,phone,email 15 | type NotExists struct { 16 | db *gorm.DB 17 | } 18 | 19 | func NewNotExists(db *gorm.DB) *NotExists { 20 | return &NotExists{db: db} 21 | } 22 | 23 | func (r *NotExists) Passes(val any, options ...any) bool { 24 | if len(options) < 2 { 25 | return false 26 | } 27 | 28 | tableName := options[0].(string) 29 | fieldNames := options[1:] 30 | 31 | query := r.db.Table(tableName).Where(fmt.Sprintf("%s = ?", fieldNames[0]), val) 32 | for _, fieldName := range fieldNames[1:] { 33 | query = query.Or(fmt.Sprintf("%s = ?", fieldName), val) 34 | } 35 | 36 | var count int64 37 | err := query.Count(&count).Error 38 | if err != nil { 39 | return false 40 | } 41 | 42 | return count == 0 43 | } 44 | -------------------------------------------------------------------------------- /internal/http/rule/password.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "unicode" 5 | 6 | "github.com/spf13/cast" 7 | ) 8 | 9 | // Password 密码复杂度校验 10 | type Password struct{} 11 | 12 | func NewPassword() *Password { 13 | return &Password{} 14 | } 15 | 16 | func (r *Password) Passes(val any, options ...any) bool { 17 | password := cast.ToString(val) 18 | // 不对空密码进行校验,有需要可以使用 required 标签 19 | if password == "" { 20 | return true 21 | } 22 | 23 | var hasUpper, hasLower, hasNumber, hasSpecial bool 24 | if len(password) < 8 || len(password) > 20 { 25 | return false 26 | } 27 | 28 | for _, char := range password { 29 | switch { 30 | case unicode.IsUpper(char): 31 | hasUpper = true 32 | case unicode.IsLower(char): 33 | hasLower = true 34 | case unicode.IsNumber(char): 35 | hasNumber = true 36 | case unicode.IsPunct(char) || unicode.IsSymbol(char): 37 | hasSpecial = true 38 | } 39 | } 40 | 41 | // 至少包含两类字符组合 42 | valid := (hasUpper && hasLower) || 43 | (hasUpper && hasNumber) || 44 | (hasUpper && hasSpecial) || 45 | (hasLower && hasNumber) || 46 | (hasLower && hasSpecial) || 47 | (hasNumber && hasSpecial) 48 | 49 | return valid 50 | } 51 | -------------------------------------------------------------------------------- /internal/http/rule/rule.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "github.com/gookit/validate" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | func GlobalRules(db *gorm.DB) { 9 | validate.AddValidators(validate.M{ 10 | "exists": NewExists(db).Passes, 11 | "notExists": NewNotExists(db).Passes, 12 | "password": NewPassword().Passes, 13 | "cron": NewCron().Passes, 14 | "ipcidr": NewIPCIDR().Passes, 15 | }) 16 | validate.AddGlobalMessages(map[string]string{ 17 | "exists": "{field} 不存在", 18 | "notExists": "{field} 已存在", 19 | "password": "密码不满足要求(8-20位,至少包含字母、数字、特殊字符中的两种)", 20 | "cron": "Cron 表达式不合法", 21 | "ipcidr": "IP 或 CIDR 格式不合法", 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /internal/job/job.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "log/slog" 5 | 6 | "github.com/google/wire" 7 | "github.com/robfig/cron/v3" 8 | "gorm.io/gorm" 9 | 10 | "github.com/tnb-labs/panel/internal/biz" 11 | ) 12 | 13 | var ProviderSet = wire.NewSet(NewJobs) 14 | 15 | type Jobs struct { 16 | db *gorm.DB 17 | log *slog.Logger 18 | setting biz.SettingRepo 19 | cert biz.CertRepo 20 | backup biz.BackupRepo 21 | cache biz.CacheRepo 22 | task biz.TaskRepo 23 | } 24 | 25 | func NewJobs(db *gorm.DB, log *slog.Logger, setting biz.SettingRepo, cert biz.CertRepo, backup biz.BackupRepo, cache biz.CacheRepo, task biz.TaskRepo) *Jobs { 26 | return &Jobs{ 27 | db: db, 28 | log: log, 29 | setting: setting, 30 | cert: cert, 31 | backup: backup, 32 | cache: cache, 33 | task: task, 34 | } 35 | } 36 | 37 | func (r *Jobs) Register(c *cron.Cron) error { 38 | if _, err := c.AddJob("* * * * *", NewMonitoring(r.db, r.log, r.setting)); err != nil { 39 | return err 40 | } 41 | if _, err := c.AddJob("0 4 * * *", NewCertRenew(r.db, r.log, r.cert)); err != nil { 42 | return err 43 | } 44 | 45 | if _, err := c.AddJob("0 2 * * *", NewPanelTask(r.db, r.log, r.backup, r.cache, r.task, r.setting)); err != nil { 46 | return err 47 | } 48 | 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /internal/migration/migration.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import "github.com/go-gormigrate/gormigrate/v2" 4 | 5 | var Migrations []*gormigrate.Migration 6 | -------------------------------------------------------------------------------- /internal/route/route.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import "github.com/google/wire" 4 | 5 | // ProviderSet is route providers. 6 | var ProviderSet = wire.NewSet(NewCli, NewHttp, NewWs) 7 | -------------------------------------------------------------------------------- /internal/route/ws.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "github.com/go-chi/chi/v5" 5 | 6 | "github.com/tnb-labs/panel/internal/service" 7 | ) 8 | 9 | type Ws struct { 10 | ws *service.WsService 11 | } 12 | 13 | func NewWs(ws *service.WsService) *Ws { 14 | return &Ws{ 15 | ws: ws, 16 | } 17 | } 18 | 19 | func (route *Ws) Register(r *chi.Mux) { 20 | r.Route("/api/ws", func(r chi.Router) { 21 | r.Get("/ssh", route.ws.Session) 22 | r.Get("/exec", route.ws.Exec) 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /internal/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "github.com/google/wire" 4 | 5 | // ProviderSet is service providers. 6 | var ProviderSet = wire.NewSet( 7 | NewAppService, 8 | NewBackupService, 9 | NewCertService, 10 | NewCertAccountService, 11 | NewCertDNSService, 12 | NewCliService, 13 | NewContainerService, 14 | NewContainerComposeService, 15 | NewContainerImageService, 16 | NewContainerNetworkService, 17 | NewContainerVolumeService, 18 | NewCronService, 19 | NewDashboardService, 20 | NewDatabaseService, 21 | NewDatabaseServerService, 22 | NewDatabaseUserService, 23 | NewFileService, 24 | NewFirewallService, 25 | NewMonitorService, 26 | NewProcessService, 27 | NewSafeService, 28 | NewSettingService, 29 | NewSSHService, 30 | NewSystemctlService, 31 | NewTaskService, 32 | NewUserService, 33 | NewUserTokenService, 34 | NewWebsiteService, 35 | NewToolboxSystemService, 36 | NewToolboxBenchmarkService, 37 | NewWsService, 38 | ) 39 | -------------------------------------------------------------------------------- /pkg/acme/client_test.go: -------------------------------------------------------------------------------- 1 | package acme 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | type ClientTestSuite struct { 12 | suite.Suite 13 | } 14 | 15 | func TestClientTestSuite(t *testing.T) { 16 | suite.Run(t, &ClientTestSuite{}) 17 | } 18 | 19 | func (s *ClientTestSuite) TestObtainSSL() { 20 | ctx := context.Background() 21 | client, err := NewRegisterAccount(ctx, "ci@haozi.net", CALetsEncryptStaging, nil, KeyEC256, slog.Default()) 22 | s.Nil(err) 23 | 24 | client.UseDns(AliYun, DNSParam{ 25 | AK: "123456", 26 | SK: "654321", 27 | }) 28 | 29 | /*client.UseManualDns(2) 30 | 31 | resolves, err := client.GetDNSRecords(ctx, []string{"*.haozi.net", "haozi.net"}, KeyEC256) 32 | debug.Dump(resolves) 33 | s.Nil(err) 34 | s.NotNil(resolves) 35 | 36 | time.Sleep(2 * time.Minute) 37 | 38 | ssl, err := client.ObtainCertificateManual()*/ 39 | ssl, err := client.ObtainCertificate(ctx, []string{"*.haozi.net", "haozi.net"}, KeyEC256) 40 | s.Error(err) 41 | s.NotNil(ssl) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/api/acme.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "fmt" 4 | 5 | type EAB struct { 6 | KeyID string `json:"key_id"` 7 | MacKey string `json:"mac_key"` 8 | } 9 | 10 | func (r *API) GoogleEAB() (*EAB, error) { 11 | resp, err := r.client.R().SetResult(&Response{}).Get("/acme/googleEAB") 12 | if err != nil { 13 | return nil, err 14 | } 15 | if !resp.IsSuccess() { 16 | return nil, fmt.Errorf("failed to get google eab: %s", resp.String()) 17 | } 18 | 19 | eab, err := getResponseData[EAB](resp) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return eab, nil 25 | } 26 | -------------------------------------------------------------------------------- /pkg/api/api_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/suite" 7 | ) 8 | 9 | type APITestSuite struct { 10 | suite.Suite 11 | api *API 12 | } 13 | 14 | func TestAPITestSuite(t *testing.T) { 15 | suite.Run(t, &APITestSuite{ 16 | api: NewAPI("2.3.0", "en"), 17 | }) 18 | } 19 | 20 | func (s *APITestSuite) TestGetLatestVersion() { 21 | _, err := s.api.LatestVersion("stable") 22 | s.NoError(err) 23 | } 24 | 25 | func (s *APITestSuite) TestGetIntermediateVersions() { 26 | _, err := s.api.IntermediateVersions("stable") 27 | s.NoError(err) 28 | } 29 | 30 | func (s *APITestSuite) TestGetApps() { 31 | _, err := s.api.Apps() 32 | s.NoError(err) 33 | } 34 | 35 | func (s *APITestSuite) TestGetAppBySlug() { 36 | _, err := s.api.AppBySlug("nginx") 37 | s.NoError(err) 38 | } 39 | 40 | func (s *APITestSuite) TestGetRewritesByType() { 41 | _, err := s.api.RewritesByType("nginx") 42 | s.NoError(err) 43 | } 44 | -------------------------------------------------------------------------------- /pkg/api/rewrite.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | type Rewrite struct { 9 | CreatedAt time.Time `json:"created_at"` 10 | UpdatedAt time.Time `json:"updated_at"` 11 | Name string `json:"name"` 12 | Type string `json:"type"` 13 | Content string `json:"content"` 14 | } 15 | 16 | type Rewrites []Rewrite 17 | 18 | func (r *API) RewritesByType(typ string) (*Rewrites, error) { 19 | resp, err := r.client.R().SetResult(&Response{}).Get(fmt.Sprintf("/rewrites/%s", typ)) 20 | if err != nil { 21 | return nil, err 22 | } 23 | if !resp.IsSuccess() { 24 | return nil, fmt.Errorf("failed to get rewrites: %s", resp.String()) 25 | } 26 | 27 | rewrites, err := getResponseData[Rewrites](resp) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | return rewrites, nil 33 | } 34 | -------------------------------------------------------------------------------- /pkg/apploader/apploader.go: -------------------------------------------------------------------------------- 1 | package apploader 2 | 3 | import ( 4 | "reflect" 5 | "slices" 6 | "strings" 7 | "sync" 8 | 9 | "github.com/go-chi/chi/v5" 10 | 11 | "github.com/tnb-labs/panel/pkg/types" 12 | ) 13 | 14 | var apps sync.Map 15 | 16 | type Loader struct{} 17 | 18 | func (r *Loader) Add(app ...types.App) { 19 | for item := range slices.Values(app) { 20 | slug := getSlug(item) 21 | apps.Store(slug, item) 22 | } 23 | } 24 | 25 | func (r *Loader) Register(mux chi.Router) { 26 | /*for slug, item := range r.Apps { 27 | mux.Route("/"+slug, item.Route) 28 | }*/ 29 | 30 | apps.Range(func(key, value any) bool { 31 | app := value.(types.App) 32 | mux.Route("/"+key.(string), app.Route) 33 | return true 34 | }) 35 | } 36 | 37 | func Slugs() []string { 38 | var slugs []string 39 | apps.Range(func(key, value any) bool { 40 | slugs = append(slugs, key.(string)) 41 | return true 42 | }) 43 | return slugs 44 | } 45 | 46 | func getSlug(app types.App) string { 47 | if app == nil { 48 | return "" 49 | } 50 | 51 | t := reflect.TypeOf(app) 52 | if t.Kind() == reflect.Ptr { 53 | t = t.Elem() 54 | } 55 | 56 | pkgPath := t.PkgPath() 57 | if pkgPath == "" { 58 | return "" 59 | } 60 | 61 | parts := strings.Split(pkgPath, "/") 62 | return parts[len(parts)-1] 63 | } 64 | -------------------------------------------------------------------------------- /pkg/cert/cert_test.go: -------------------------------------------------------------------------------- 1 | package cert 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/suite" 7 | ) 8 | 9 | type CertTestSuite struct { 10 | suite.Suite 11 | } 12 | 13 | func TestCertTestSuite(t *testing.T) { 14 | suite.Run(t, &CertTestSuite{}) 15 | } 16 | 17 | func (s *CertTestSuite) TestGenerateSelfSigned() { 18 | pem, key, err := GenerateSelfSigned([]string{"haozi.dev"}) 19 | s.Nil(err) 20 | s.NotNil(pem) 21 | s.NotNil(key) 22 | } 23 | -------------------------------------------------------------------------------- /pkg/cron/logger.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "log/slog" 5 | ) 6 | 7 | type Logger struct { 8 | log *slog.Logger 9 | debug bool 10 | } 11 | 12 | func NewLogger(log *slog.Logger, debug bool) *Logger { 13 | return &Logger{ 14 | debug: debug, 15 | log: log, 16 | } 17 | } 18 | 19 | func (log *Logger) Info(msg string, keysAndValues ...any) { 20 | if !log.debug { 21 | return 22 | } 23 | 24 | log.log.Info(msg, keysAndValues...) 25 | } 26 | 27 | func (log *Logger) Error(err error, msg string, keysAndValues ...any) { 28 | fields := []any{slog.Any("err", err)} 29 | fields = append(fields, log.toSlogArgs(keysAndValues...)...) 30 | log.log.Error(msg, fields...) 31 | } 32 | 33 | func (log *Logger) toSlogArgs(keysAndValues ...any) []any { 34 | fields := make([]any, 0, len(keysAndValues)/2) 35 | for i := 0; i < len(keysAndValues); i += 2 { 36 | if i+1 < len(keysAndValues) { 37 | key, ok := keysAndValues[i].(string) 38 | if ok { 39 | fields = append(fields, slog.Any(key, keysAndValues[i+1])) 40 | } 41 | } 42 | } 43 | return fields 44 | } 45 | -------------------------------------------------------------------------------- /pkg/db/mysql_tools.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/tnb-labs/panel/pkg/shell" 7 | "github.com/tnb-labs/panel/pkg/systemctl" 8 | ) 9 | 10 | // MySQLResetRootPassword 重置 MySQL root密码 11 | func MySQLResetRootPassword(password string) error { 12 | _ = systemctl.Stop("mysqld") 13 | if run, err := systemctl.Status("mysqld"); err != nil || run { 14 | return fmt.Errorf("failed to stop MySQL: %w", err) 15 | } 16 | _, _ = shell.Execf(`systemctl set-environment MYSQLD_OPTS="--skip-grant-tables --skip-networking"`) 17 | if err := systemctl.Start("mysqld"); err != nil { 18 | return fmt.Errorf("failed to start MySQL in safe mode: %w", err) 19 | } 20 | if _, err := shell.Execf(`mysql -uroot -e "FLUSH PRIVILEGES;UPDATE mysql.user SET authentication_string=null WHERE user='root' AND host='localhost';ALTER USER 'root'@'localhost' IDENTIFIED BY '%s';FLUSH PRIVILEGES;"`, password); err != nil { 21 | return fmt.Errorf("failed to reset MySQL root password: %w", err) 22 | } 23 | if err := systemctl.Stop("mysqld"); err != nil { 24 | return fmt.Errorf("failed to stop MySQL: %w", err) 25 | } 26 | _, _ = shell.Execf(`systemctl unset-environment MYSQLD_OPTS`) 27 | if err := systemctl.Start("mysqld"); err != nil { 28 | return fmt.Errorf("failed to start MySQL: %w", err) 29 | } 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /pkg/embed/.gitignore: -------------------------------------------------------------------------------- 1 | frontend/* 2 | !frontend/.gitkeep -------------------------------------------------------------------------------- /pkg/embed/embed.go: -------------------------------------------------------------------------------- 1 | package embed 2 | 3 | import "embed" 4 | 5 | //go:embed all:frontend/* 6 | var PublicFS embed.FS 7 | 8 | //go:embed all:website/* 9 | var WebsiteFS embed.FS 10 | 11 | //go:embed all:locales/* 12 | var LocalesFS embed.FS 13 | -------------------------------------------------------------------------------- /pkg/embed/frontend/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tnb-labs/panel/f3cec5e476fbe0fdbd6567ea67ca2bfa1745c258/pkg/embed/frontend/.gitkeep -------------------------------------------------------------------------------- /pkg/nginx/testdata/http.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | index index.php index.html index.htm; 5 | root /www/wwwroot/default; 6 | # Error page configuration 7 | error_page 404 /404.html; 8 | include enable-php-0.conf; 9 | # Do not log static files 10 | location ~ .*\.(bmp|jpg|jpeg|png|gif|svg|ico|tiff|webp|avif|heif|heic|jxl)$ { 11 | expires 30d; 12 | access_log /dev/null; 13 | error_log /dev/null; 14 | } 15 | location ~ .*\.(js|css|ttf|otf|woff|woff2|eot)$ { 16 | expires 6h; 17 | access_log /dev/null; 18 | error_log /dev/null; 19 | } 20 | # Deny some sensitive directories 21 | location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.env) { 22 | return 404; 23 | } 24 | access_log /www/wwwlogs/default.log; 25 | error_log /www/wwwlogs/default.log; 26 | } -------------------------------------------------------------------------------- /pkg/ntp/ntp_test.go: -------------------------------------------------------------------------------- 1 | package ntp 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/go-rat/utils/env" 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | type NTPTestSuite struct { 12 | suite.Suite 13 | } 14 | 15 | func TestNTPTestSuite(t *testing.T) { 16 | suite.Run(t, &NTPTestSuite{}) 17 | } 18 | 19 | func (suite *NTPTestSuite) TestNowWithDefaultAddresses() { 20 | now, _ := Now() 21 | suite.WithinDuration(time.Now(), now, time.Minute) 22 | } 23 | 24 | func (suite *NTPTestSuite) TestNowWithCustomAddress() { 25 | now, err := Now("time.windows.com") 26 | suite.NoError(err) 27 | suite.WithinDuration(time.Now(), now, time.Minute) 28 | } 29 | 30 | func (suite *NTPTestSuite) TestNowWithInvalidAddress() { 31 | _, err := Now("invalid.address") 32 | suite.Error(err) 33 | } 34 | 35 | func (suite *NTPTestSuite) TestUpdateSystemTime() { 36 | if env.IsWindows() { 37 | suite.T().Skip("Skipping on Windows") 38 | } 39 | err := UpdateSystemTime(time.Now()) 40 | suite.NoError(err) 41 | } 42 | 43 | func (suite *NTPTestSuite) TestUpdateSystemTimeZone() { 44 | if env.IsWindows() { 45 | suite.T().Skip("Skipping on Windows") 46 | } 47 | err := UpdateSystemTimeZone("UTC") 48 | suite.NoError(err) 49 | } 50 | -------------------------------------------------------------------------------- /pkg/os/os_test.go: -------------------------------------------------------------------------------- 1 | package os 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/suite" 7 | ) 8 | 9 | type OSHelperTestSuite struct { 10 | suite.Suite 11 | } 12 | 13 | func TestOSHelperTestSuite(t *testing.T) { 14 | suite.Run(t, &OSHelperTestSuite{}) 15 | } 16 | 17 | func (s *OSHelperTestSuite) TestIsDebian() { 18 | s.True(IsDebian()) 19 | } 20 | 21 | func (s *OSHelperTestSuite) TestIsRHEL() { 22 | s.False(IsRHEL()) 23 | } 24 | -------------------------------------------------------------------------------- /pkg/os/user.go: -------------------------------------------------------------------------------- 1 | package os 2 | 3 | import ( 4 | "os/user" 5 | 6 | "github.com/spf13/cast" 7 | ) 8 | 9 | // GetUser 通过 uid 获取用户名 10 | func GetUser(uid uint32) string { 11 | id := cast.ToString(uid) 12 | usr, err := user.LookupId(id) 13 | if err != nil { 14 | return id 15 | } 16 | return usr.Username 17 | } 18 | 19 | // GetGroup 通过 gid 获取组名 20 | func GetGroup(gid uint32) string { 21 | id := cast.ToString(gid) 22 | usr, err := user.LookupGroupId(id) 23 | if err != nil { 24 | return id 25 | } 26 | return usr.Name 27 | } 28 | -------------------------------------------------------------------------------- /pkg/queue/job.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | type Job interface { 4 | Handle(args ...any) error 5 | } 6 | 7 | type JobWithErrHandle interface { 8 | Job 9 | ErrHandle(err error) 10 | } 11 | 12 | type JobItem struct { 13 | Job Job 14 | Args []any 15 | Delay uint 16 | } 17 | -------------------------------------------------------------------------------- /pkg/rsacrypto/rsacrypto_test.go: -------------------------------------------------------------------------------- 1 | package rsacrypto 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/suite" 7 | ) 8 | 9 | type RSATestSuite struct { 10 | suite.Suite 11 | } 12 | 13 | func TestRSATestSuite(t *testing.T) { 14 | suite.Run(t, &RSATestSuite{}) 15 | } 16 | 17 | func (suite *RSATestSuite) TestRSA() { 18 | // 生成RSA密钥对 19 | privateKey, err := GenerateKey() 20 | suite.NoError(err) 21 | suite.NotEmpty(privateKey) 22 | suite.NotEmpty(privateKey.PublicKey) 23 | 24 | // 提取密钥对 25 | suite.NotEmpty(PrivateKeyToString(privateKey)) 26 | suite.NotEmpty(PublicKeyToString(&privateKey.PublicKey)) 27 | 28 | message := []byte("Rat Panel") 29 | 30 | // 加密数据 31 | ciphertext, err := EncryptData(&privateKey.PublicKey, message) 32 | suite.NoError(err) 33 | suite.NotEmpty(ciphertext) 34 | 35 | // 解密数据 36 | decrypted, err := DecryptData(privateKey, ciphertext) 37 | suite.NoError(err) 38 | suite.NotEmpty(decrypted) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/tools/logger.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | // NoopLogger 给 go-resty 使用的空日志 4 | type NoopLogger struct{} 5 | 6 | func (NoopLogger) Errorf(format string, v ...any) {} 7 | 8 | func (NoopLogger) Warnf(format string, v ...any) {} 9 | 10 | func (NoopLogger) Debugf(format string, v ...any) {} 11 | -------------------------------------------------------------------------------- /pkg/types/app.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "github.com/go-chi/chi/v5" 4 | 5 | // App 应用接口 6 | type App interface { 7 | Route(r chi.Router) 8 | } 9 | 10 | // AppCenter 应用中心结构 11 | type AppCenter struct { 12 | Icon string `json:"icon"` 13 | Name string `json:"name"` 14 | Description string `json:"description"` 15 | Slug string `json:"slug"` 16 | Channels []struct { 17 | Slug string `json:"slug"` 18 | Name string `json:"name"` 19 | Panel string `json:"panel"` 20 | Install string `json:"-"` 21 | Uninstall string `json:"-"` 22 | Update string `json:"-"` 23 | Subs []struct { 24 | Log string `json:"log"` 25 | Version string `json:"version"` 26 | } `json:"subs"` 27 | } `json:"channels"` 28 | Installed bool `json:"installed"` 29 | InstalledChannel string `json:"installed_channel"` 30 | InstalledVersion string `json:"installed_version"` 31 | UpdateExist bool `json:"update_exist"` 32 | Show bool `json:"show"` 33 | } 34 | -------------------------------------------------------------------------------- /pkg/types/backup.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "time" 4 | 5 | type BackupFile struct { 6 | Name string `json:"name"` 7 | Path string `json:"path"` 8 | Size string `json:"size"` 9 | Time time.Time `json:"time"` 10 | } 11 | -------------------------------------------------------------------------------- /pkg/types/cert.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "time" 4 | 5 | type CertList struct { 6 | ID uint `json:"id"` 7 | AccountID uint `json:"account_id"` 8 | WebsiteID uint `json:"website_id"` 9 | DNSID uint `json:"dns_id"` 10 | Type string `json:"type"` 11 | Domains []string `json:"domains"` 12 | AutoRenew bool `json:"auto_renew"` 13 | Cert string `json:"cert"` 14 | Key string `json:"key"` 15 | CertURL string `json:"cert_url"` 16 | Script string `json:"script"` 17 | NotBefore time.Time `json:"not_before"` 18 | NotAfter time.Time `json:"not_after"` 19 | Issuer string `json:"issuer"` 20 | OCSPServer []string `json:"ocsp_server"` 21 | DNSNames []string `json:"dns_names"` 22 | CreatedAt time.Time `json:"created_at"` 23 | UpdatedAt time.Time `json:"updated_at"` 24 | } 25 | -------------------------------------------------------------------------------- /pkg/types/config.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // PanelConfig 面板配置结构体 4 | type PanelConfig struct { 5 | App PanelAppConfig `yaml:"app"` 6 | HTTP PanelHTTPConfig `yaml:"http"` 7 | Database PanelDatabaseConfig `yaml:"database"` 8 | Session PanelSessionConfig `yaml:"session"` 9 | } 10 | 11 | type PanelAppConfig struct { 12 | Debug bool `yaml:"debug"` 13 | Key string `yaml:"key"` 14 | Locale string `yaml:"locale"` 15 | Timezone string `yaml:"timezone"` 16 | Root string `yaml:"root"` 17 | } 18 | 19 | type PanelHTTPConfig struct { 20 | Debug bool `yaml:"debug"` 21 | Port uint `yaml:"port"` 22 | Entrance string `yaml:"entrance"` 23 | TLS bool `yaml:"tls"` 24 | BindDomain []string `yaml:"bind_domain"` 25 | BindIP []string `yaml:"bind_ip"` 26 | BindUA []string `yaml:"bind_ua"` 27 | } 28 | 29 | type PanelDatabaseConfig struct { 30 | Debug bool `yaml:"debug"` 31 | } 32 | 33 | type PanelSessionConfig struct { 34 | Lifetime uint `yaml:"lifetime"` 35 | } 36 | -------------------------------------------------------------------------------- /pkg/types/container_compose.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "time" 4 | 5 | // ContainerComposeRaw docker compose ls 命令原始输出 6 | type ContainerComposeRaw struct { 7 | Name string `json:"Name"` 8 | Status string `json:"Status"` 9 | ConfigFiles string `json:"ConfigFiles"` 10 | } 11 | 12 | type ContainerCompose struct { 13 | Name string `json:"name"` 14 | Path string `json:"path"` 15 | Status string `json:"status"` 16 | CreatedAt time.Time `json:"created_at"` 17 | } 18 | -------------------------------------------------------------------------------- /pkg/types/container_image.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type ContainerImage struct { 8 | ID string `json:"id"` 9 | Containers int64 `json:"containers"` 10 | RepoTags []string `json:"repo_tags"` 11 | RepoDigests []string `json:"repo_digests"` 12 | Size string `json:"size"` 13 | Labels []KV `json:"labels"` 14 | CreatedAt time.Time `json:"created_at"` 15 | } 16 | -------------------------------------------------------------------------------- /pkg/types/container_volume.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type ContainerVolume struct { 8 | Name string `json:"name"` 9 | Driver string `json:"driver"` 10 | Scope string `json:"scope"` 11 | MountPoint string `json:"mount_point"` 12 | CreatedAt time.Time `json:"created_at"` 13 | Labels []KV `json:"labels"` 14 | Options []KV `json:"options"` 15 | RefCount int64 `json:"ref_count"` 16 | Size string `json:"size"` 17 | } 18 | -------------------------------------------------------------------------------- /pkg/types/docker/network/ipam.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | // IPAM represents IP Address Management 4 | type IPAM struct { 5 | Driver string 6 | Options map[string]string // Per network IPAM driver options 7 | Config []IPAMConfig 8 | } 9 | 10 | // IPAMConfig represents IPAM configurations 11 | type IPAMConfig struct { 12 | Subnet string `json:",omitempty"` 13 | IPRange string `json:",omitempty"` 14 | Gateway string `json:",omitempty"` 15 | AuxAddress map[string]string `json:"AuxiliaryAddresses,omitempty"` 16 | } 17 | -------------------------------------------------------------------------------- /pkg/types/docker/port.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | // Port An open port on a container 4 | type Port struct { 5 | 6 | // Host IP address that the container's port is mapped to 7 | IP string `json:"IP,omitempty"` 8 | 9 | // Port on the container 10 | // Required: true 11 | PrivatePort uint16 `json:"PrivatePort"` 12 | 13 | // Port exposed on the host 14 | PublicPort uint16 `json:"PublicPort,omitempty"` 15 | 16 | // type 17 | // Required: true 18 | Type string `json:"Type"` 19 | } 20 | -------------------------------------------------------------------------------- /pkg/types/docker/swarm/meta.go: -------------------------------------------------------------------------------- 1 | package swarm 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | ) 7 | 8 | // Version represents the internal object version. 9 | type Version struct { 10 | Index uint64 `json:",omitempty"` 11 | } 12 | 13 | // String implements fmt.Stringer interface. 14 | func (v Version) String() string { 15 | return strconv.FormatUint(v.Index, 10) 16 | } 17 | 18 | // Meta is a base object inherited by most of the other once. 19 | type Meta struct { 20 | Version Version `json:",omitempty"` 21 | CreatedAt time.Time `json:",omitempty"` 22 | UpdatedAt time.Time `json:",omitempty"` 23 | } 24 | -------------------------------------------------------------------------------- /pkg/types/docker/volume/list_response.go: -------------------------------------------------------------------------------- 1 | package volume 2 | 3 | // This file was generated by the swagger tool. 4 | // Editing this file might prove futile when you re-run the swagger generate command 5 | 6 | // ListResponse VolumeListResponse 7 | // 8 | // Volume list response 9 | // swagger:model ListResponse 10 | type ListResponse struct { 11 | 12 | // List of volumes 13 | Volumes []*Volume `json:"Volumes"` 14 | 15 | // Warnings that occurred when fetching the list of volumes. 16 | // 17 | Warnings []string `json:"Warnings"` 18 | } 19 | -------------------------------------------------------------------------------- /pkg/types/monitor.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type Load struct { 4 | Load1 []float64 `json:"load1"` 5 | Load5 []float64 `json:"load5"` 6 | Load15 []float64 `json:"load15"` 7 | } 8 | 9 | type CPU struct { 10 | Percent []string `json:"percent"` 11 | } 12 | 13 | type Mem struct { 14 | Total string `json:"total"` 15 | Available []string `json:"available"` 16 | Used []string `json:"used"` 17 | } 18 | 19 | type SWAP struct { 20 | Total string `json:"total"` 21 | Used []string `json:"used"` 22 | Free []string `json:"free"` 23 | } 24 | 25 | type Network struct { 26 | Sent []string `json:"sent"` 27 | Recv []string `json:"recv"` 28 | Tx []string `json:"tx"` 29 | Rx []string `json:"rx"` 30 | } 31 | 32 | type MonitorData struct { 33 | Times []string `json:"times"` 34 | Load Load `json:"load"` 35 | CPU CPU `json:"cpu"` 36 | Mem Mem `json:"mem"` 37 | SWAP SWAP `json:"swap"` 38 | Net Network `json:"net"` 39 | } 40 | -------------------------------------------------------------------------------- /pkg/types/mysql.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type MySQLUser struct { 4 | User string `json:"user"` 5 | Host string `json:"host"` 6 | Grants []string `json:"grants"` 7 | } 8 | 9 | type MySQLDatabase struct { 10 | Name string `json:"name"` 11 | CharSet string `json:"char_set"` 12 | Collation string `json:"collation"` 13 | } 14 | -------------------------------------------------------------------------------- /pkg/types/postgres.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type PostgresUser struct { 4 | Role string `json:"role"` 5 | Attributes []string `json:"attributes"` 6 | } 7 | 8 | type PostgresDatabase struct { 9 | Name string `json:"name"` 10 | Owner string `json:"owner"` 11 | Encoding string `json:"encoding"` 12 | Comment string `json:"comment"` 13 | } 14 | -------------------------------------------------------------------------------- /pkg/types/process.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "github.com/shirou/gopsutil/net" 5 | "github.com/shirou/gopsutil/process" 6 | ) 7 | 8 | type ProcessData struct { 9 | PID int32 `json:"pid"` 10 | Name string `json:"name"` 11 | PPID int32 `json:"ppid"` 12 | Username string `json:"username"` 13 | Status string `json:"status"` 14 | Background bool `json:"background"` 15 | StartTime string `json:"start_time"` 16 | NumThreads int32 `json:"num_threads"` 17 | CPU float64 `json:"cpu"` 18 | 19 | DiskRead uint64 `json:"disk_read"` 20 | DiskWrite uint64 `json:"disk_write"` 21 | 22 | CmdLine string `json:"cmd_line"` 23 | 24 | RSS uint64 `json:"rss"` 25 | VMS uint64 `json:"vms"` 26 | HWM uint64 `json:"hwm"` 27 | Data uint64 `json:"data"` 28 | Stack uint64 `json:"stack"` 29 | Locked uint64 `json:"locked"` 30 | Swap uint64 `json:"swap"` 31 | 32 | Envs []string `json:"envs"` 33 | 34 | OpenFiles []process.OpenFilesStat `json:"open_files"` 35 | Connections []net.ConnectionStat `json:"connections"` 36 | Nets []net.IOCountersStat `json:"nets"` 37 | } 38 | -------------------------------------------------------------------------------- /pkg/types/system.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/shirou/gopsutil/cpu" 7 | "github.com/shirou/gopsutil/disk" 8 | "github.com/shirou/gopsutil/host" 9 | "github.com/shirou/gopsutil/load" 10 | "github.com/shirou/gopsutil/mem" 11 | "github.com/shirou/gopsutil/net" 12 | ) 13 | 14 | // CurrentInfo 监控信息 15 | type CurrentInfo struct { 16 | Cpus []cpu.InfoStat `json:"cpus"` 17 | Percent float64 `json:"percent"` // 总使用率 18 | Percents []float64 `json:"percents"` // 每个核心使用率 19 | Load *load.AvgStat `json:"load"` 20 | Host *host.InfoStat `json:"host"` 21 | Mem *mem.VirtualMemoryStat `json:"mem"` 22 | Swap *mem.SwapMemoryStat `json:"swap"` 23 | Net []net.IOCountersStat `json:"net"` 24 | DiskIO []disk.IOCountersStat `json:"disk_io"` 25 | Disk []disk.PartitionStat `json:"disk"` 26 | DiskUsage []disk.UsageStat `json:"disk_usage"` 27 | Time time.Time `json:"time"` 28 | } 29 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "labels": [ 7 | "🤖 Dependencies" 8 | ], 9 | "commitMessagePrefix": "chore(deps): ", 10 | "lockFileMaintenance": { 11 | "enabled": true, 12 | "automerge": true 13 | }, 14 | "platformAutomerge": true, 15 | "postUpdateOptions": [ 16 | "gomodTidy", 17 | "gomodUpdateImportPaths", 18 | "pnpmDedupe" 19 | ], 20 | "packageRules": [ 21 | { 22 | "groupName": "non-major dependencies", 23 | "matchUpdateTypes": [ 24 | "digest", 25 | "pin", 26 | "patch", 27 | "minor" 28 | ], 29 | "automerge": true 30 | } 31 | ], 32 | "ignoreDeps": [ 33 | "github.com/libdns/libdns", 34 | "github.com/libdns/cloudflare", 35 | "github.com/libdns/tencentcloud", 36 | "github.com/libdns/duckdns", 37 | "github.com/libdns/gcore" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /storage/logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tnb-labs/panel/f3cec5e476fbe0fdbd6567ea67ca2bfa1745c258/storage/logs/.gitkeep -------------------------------------------------------------------------------- /web/.env.development: -------------------------------------------------------------------------------- 1 | VITE_APP_TITLE = '耗子面板' 2 | 3 | # 资源公共路径,需要以 /开头和结尾 4 | VITE_PUBLIC_PATH = '/' 5 | 6 | # 是否hash路由模式 7 | VITE_USE_HASH = false 8 | 9 | # base api 10 | VITE_BASE_API = '/api' 11 | 12 | # 是否启用代理(只对本地vite server生效) 13 | VITE_USE_PROXY = true 14 | 15 | # 代理类型(跟启动和构建环境无关) 'dev' | 'test' | 'prod' 16 | VITE_PROXY_TYPE = 'dev' 17 | 18 | -------------------------------------------------------------------------------- /web/.env.production: -------------------------------------------------------------------------------- 1 | VITE_APP_TITLE = '耗子面板' 2 | 3 | # 资源公共路径,需要以 /开头和结尾 4 | VITE_PUBLIC_PATH = '/' 5 | 6 | # 是否hash路由模式 7 | VITE_USE_HASH = false 8 | 9 | # base api 10 | VITE_BASE_API = '/api' 11 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | 18 | # Editor directories and files 19 | .vscode 20 | .idea 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | .env 28 | 29 | .eslintrc-auto-import.json 30 | types/components.d.ts 31 | types/auto-imports.d.ts 32 | -------------------------------------------------------------------------------- /web/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": false, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "trailingComma": "none", 8 | "plugins": [ 9 | "prettier-plugin-organize-imports" 10 | ] 11 | } -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # 耗子面板 2 | 3 | 这是耗子面板的前端部分,使用 Vue3 + Vite + UnoCSS + Naive UI 开发。 4 | 5 | 预了解更多请移步 [耗子面板](https://github.com/tnb-labs/panel)。 6 | 7 | # Rat Panel 8 | 9 | This is the frontend part of Rat Panel, developed using Vue3 + Vite + UnoCSS + Naive UI. 10 | 11 | For more information, please visit [Rat Panel](https://github.com/tnb-labs/panel). 12 | -------------------------------------------------------------------------------- /web/build/config/define.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from 'luxon' 2 | 3 | /** 4 | * 此处定义的是全局常量,启动或打包后将添加到 window 中 5 | * https://vitejs.cn/config/#define 6 | */ 7 | 8 | // 项目构建时间 9 | const _BUILD_TIME_ = JSON.stringify(DateTime.now().toFormat('yyyy-MM-dd HH:mm:ss')) 10 | 11 | export const viteDefine = { 12 | _BUILD_TIME_ 13 | } 14 | -------------------------------------------------------------------------------- /web/build/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './define' 2 | export * from './proxy' 3 | -------------------------------------------------------------------------------- /web/build/config/proxy.ts: -------------------------------------------------------------------------------- 1 | import type { ProxyOptions } from 'vite' 2 | import { getProxyConfigs } from '../../settings/proxy-config' 3 | 4 | export function createViteProxy(isUseProxy = true, proxyType: ProxyType) { 5 | if (!isUseProxy) return undefined 6 | 7 | const proxyConfigs = getProxyConfigs(proxyType) 8 | const proxy: Record = {} 9 | 10 | proxyConfigs.forEach((proxyConfig) => { 11 | proxy[proxyConfig.prefix] = { 12 | target: proxyConfig.target, 13 | secure: proxyConfig.secure, 14 | changeOrigin: true, 15 | rewrite: (path: string) => path.replace(new RegExp(`^${proxyConfig.prefix}`), '') 16 | } 17 | }) 18 | 19 | return proxy 20 | } 21 | -------------------------------------------------------------------------------- /web/build/plugins/copy.ts: -------------------------------------------------------------------------------- 1 | import { viteStaticCopy } from 'vite-plugin-static-copy' 2 | 3 | export function setupStaticCopyPlugin() { 4 | return viteStaticCopy({ 5 | targets: [ 6 | { 7 | src: 'node_modules/monaco-editor/min/vs', 8 | dest: 'assets' 9 | } 10 | ] 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /web/build/plugins/html.ts: -------------------------------------------------------------------------------- 1 | import { createHtmlPlugin } from 'vite-plugin-html' 2 | 3 | export function setupHtmlPlugin(viteEnv: ViteEnv) { 4 | const { VITE_APP_TITLE } = viteEnv 5 | return createHtmlPlugin({ 6 | minify: true, 7 | inject: { 8 | data: { 9 | title: VITE_APP_TITLE 10 | } 11 | } 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /web/build/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import type { PluginOption } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import unocss from 'unocss/vite' 4 | import vueDevTools from 'vite-plugin-vue-devtools' 5 | 6 | import { setupStaticCopyPlugin } from './copy' 7 | import { setupHtmlPlugin } from './html' 8 | import unplugins from './unplugin' 9 | 10 | export function setupVitePlugins(viteEnv: ViteEnv): PluginOption[] { 11 | return [vue(), vueDevTools(), ...unplugins, unocss(), setupStaticCopyPlugin(), setupHtmlPlugin(viteEnv)] 12 | } 13 | -------------------------------------------------------------------------------- /web/build/utils.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | 3 | /** 4 | * * 项目根路径 5 | * @descrition 结尾不带/ 6 | */ 7 | export function getRootPath() { 8 | return path.resolve(process.cwd()) 9 | } 10 | 11 | /** 12 | * * 项目src路径 13 | * @param srcName src目录名称(默认: "src") 14 | * @descrition 结尾不带斜杠 15 | */ 16 | export function getSrcPath(srcName = 'src') { 17 | return path.resolve(getRootPath(), srcName) 18 | } 19 | 20 | /** 21 | * * 转换env配置 22 | * @param envOptions 23 | * @descrition boolean和数字类型转换 24 | */ 25 | export function convertEnv(envOptions: Record): ViteEnv { 26 | const result: any = {} 27 | if (!envOptions) return result 28 | 29 | for (const envKey in envOptions) { 30 | let envVal = envOptions[envKey] 31 | if (['true', 'false'].includes(envVal)) envVal = envVal === 'true' 32 | 33 | if (['VITE_PORT'].includes(envKey)) envVal = +envVal 34 | 35 | result[envKey] = envVal 36 | } 37 | return result 38 | } 39 | -------------------------------------------------------------------------------- /web/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_API_URL: string 5 | // 更多环境变量... 6 | } 7 | 8 | interface ImportMeta { 9 | readonly env: ImportMetaEnv 10 | } 11 | -------------------------------------------------------------------------------- /web/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { FlatCompat } from '@eslint/eslintrc' 2 | import unocss from '@unocss/eslint-config/flat' 3 | import skipFormatting from '@vue/eslint-config-prettier/skip-formatting' 4 | import vueTsEslintConfig from '@vue/eslint-config-typescript' 5 | import pluginVue from 'eslint-plugin-vue' 6 | 7 | const compat = new FlatCompat() 8 | 9 | export default [ 10 | ...pluginVue.configs['flat/essential'], 11 | ...vueTsEslintConfig(), 12 | unocss, 13 | ...compat.extends('./.eslintrc-auto-import.json'), 14 | skipFormatting, 15 | { 16 | name: 'app/files-to-lint', 17 | files: ['**/*.{ts,mts,tsx,vue}'], 18 | rules: { 19 | '@typescript-eslint/no-explicit-any': 'off', 20 | '@typescript-eslint/no-unused-vars': 'off', 21 | '@typescript-eslint/no-unused-expressions': 'off', 22 | '@typescript-eslint/no-empty-function': 'off', 23 | '@typescript-eslint/no-non-null-assertion': 'off', 24 | '@typescript-eslint/no-empty-object-type': 'off' 25 | } 26 | }, 27 | { 28 | name: 'app/files-to-ignore', 29 | ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'] 30 | } 31 | ] 32 | -------------------------------------------------------------------------------- /web/gen-auto-import.ts: -------------------------------------------------------------------------------- 1 | import unplugin from './build/plugins/unplugin' 2 | 3 | function genAutoImport() { 4 | const autoImport = unplugin[0] 5 | autoImport.buildStart.call({ 6 | root: process.cwd() 7 | }) 8 | } 9 | 10 | genAutoImport() 11 | -------------------------------------------------------------------------------- /web/gettext.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | input: { 3 | include: ['**/*.js', '**/*.ts', '**/*.vue'], 4 | exclude: ['utils/gettext/**'] 5 | }, 6 | output: { 7 | path: './src/locales', 8 | potPath: './frontend.pot', 9 | locales: ['en', 'zh_CN', 'zh_TW'], 10 | linguas: false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <%= title %> 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | <%= title %> 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /web/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - '@parcel/watcher' 3 | - esbuild 4 | - vue-demi 5 | -------------------------------------------------------------------------------- /web/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tnb-labs/panel/f3cec5e476fbe0fdbd6567ea67ca2bfa1745c258/web/public/favicon.png -------------------------------------------------------------------------------- /web/public/loading/index.js: -------------------------------------------------------------------------------- 1 | function addThemeColorCssVars() { 2 | const key = '__THEME_COLOR__' 3 | const defaultColor = '#00BFFF' 4 | const themeColor = window.localStorage.getItem(key) || defaultColor 5 | const cssVars = `--primary-color: ${themeColor}` 6 | document.documentElement.style.cssText = cssVars 7 | } 8 | 9 | addThemeColorCssVars() 10 | -------------------------------------------------------------------------------- /web/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /web/settings/.gitignore: -------------------------------------------------------------------------------- 1 | proxy-config.ts -------------------------------------------------------------------------------- /web/settings/proxy-config.example.ts: -------------------------------------------------------------------------------- 1 | const proxyConfigMappings: Record = { 2 | dev: [ 3 | { 4 | prefix: '/api/ws', 5 | target: 'ws://localhost:8888/api/ws', 6 | changeOrigin: true, 7 | secure: false, 8 | ws: true 9 | }, 10 | { 11 | prefix: '/api', 12 | target: 'http://localhost:8080/api', 13 | changeOrigin: true, 14 | secure: false 15 | } 16 | ], 17 | test: [ 18 | { 19 | prefix: '/api', 20 | target: 'http://localhost:8080/api' 21 | } 22 | ], 23 | prod: [ 24 | { 25 | prefix: '/api', 26 | target: 'http://localhost:8080/api' 27 | } 28 | ] 29 | } 30 | 31 | export function getProxyConfigs(envType: ProxyType = 'dev'): ProxyConfig[] { 32 | return proxyConfigMappings[envType] 33 | } 34 | -------------------------------------------------------------------------------- /web/settings/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "isMobile": false, 3 | "darkMode": false, 4 | "sider": { 5 | "width": 180, 6 | "collapsedWidth": 64, 7 | "collapsed": false 8 | }, 9 | "tab": { 10 | "visible": true, 11 | "height": 50 12 | }, 13 | "header": { 14 | "visible": true, 15 | "height": 60 16 | }, 17 | "primaryColor": "#00BFFF", 18 | "otherColor": { 19 | "info": "#2080F0", 20 | "success": "#18A058", 21 | "warning": "#F0A020", 22 | "error": "#D03050" 23 | }, 24 | "locale": "zh_CN", 25 | "name": "耗子面板" 26 | } 27 | -------------------------------------------------------------------------------- /web/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /web/src/api/apps/codeserver/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 获取配置 5 | config: (): any => http.Get('/apps/codeserver/config'), 6 | // 保存配置 7 | saveConfig: (config: string): any => http.Post('/apps/codeserver/config', { config }) 8 | } 9 | -------------------------------------------------------------------------------- /web/src/api/apps/docker/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | config: (): any => http.Get('/apps/docker/config'), 5 | updateConfig: (config: string): any => http.Post('/apps/docker/config', { config }) 6 | } 7 | -------------------------------------------------------------------------------- /web/src/api/apps/fail2ban/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 保护列表 5 | jails: (page: number, limit: number): any => 6 | http.Get('/apps/fail2ban/jails', { params: { page, limit } }), 7 | // 添加保护 8 | add: (data: any): any => http.Post('/apps/fail2ban/jails', data), 9 | // 删除保护 10 | delete: (name: string): any => http.Delete('/apps/fail2ban/jails', { name }), 11 | // 封禁列表 12 | jail: (name: string): any => http.Get('/apps/fail2ban/jails/' + name), 13 | // 解封 IP 14 | unban: (name: string, ip: string): any => http.Post('/apps/fail2ban/unban', { name, ip }), 15 | // 获取白名单 16 | whitelist: (): any => http.Get('/apps/fail2ban/white_list'), 17 | // 设置白名单 18 | setWhitelist: (ip: string): any => http.Post('/apps/fail2ban/white_list', { ip }) 19 | } 20 | -------------------------------------------------------------------------------- /web/src/api/apps/frp/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 获取配置 5 | config: (name: string): any => http.Get('/apps/frp/config', { params: { name } }), 6 | // 保存配置 7 | saveConfig: (name: string, config: string): any => http.Post('/apps/frp/config', { name, config }) 8 | } 9 | -------------------------------------------------------------------------------- /web/src/api/apps/gitea/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 获取配置 5 | config: (): any => http.Get('/apps/gitea/config'), 6 | // 保存配置 7 | saveConfig: (config: string): any => http.Post('/apps/gitea/config', { config }) 8 | } 9 | -------------------------------------------------------------------------------- /web/src/api/apps/memcached/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | load: (): any => http.Get('/apps/memcached/load'), 5 | config: (): any => http.Get('/apps/memcached/config'), 6 | updateConfig: (config: string): any => http.Post('/apps/memcached/config', { config }) 7 | } 8 | -------------------------------------------------------------------------------- /web/src/api/apps/minio/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 获取环境变量 5 | env: (): any => http.Get('/apps/minio/env'), 6 | // 保存环境变量 7 | saveEnv: (env: string): any => http.Post('/apps/minio/env', { env }) 8 | } 9 | -------------------------------------------------------------------------------- /web/src/api/apps/mysql/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 负载状态 5 | load: (): any => http.Get('/apps/mysql/load'), 6 | // 获取配置 7 | config: (): any => http.Get('/apps/mysql/config'), 8 | // 保存配置 9 | saveConfig: (config: string): any => http.Post('/apps/mysql/config', { config }), 10 | // 清空错误日志 11 | clearErrorLog: (): any => http.Post('/apps/mysql/clear_error_log'), 12 | // 获取慢查询日志 13 | slowLog: (): any => http.Get('/apps/mysql/slow_log'), 14 | // 清空慢查询日志 15 | clearSlowLog: (): any => http.Post('/apps/mysql/clear_slow_log'), 16 | // 获取 root 密码 17 | rootPassword: (): any => http.Get('/apps/mysql/root_password'), 18 | // 修改 root 密码 19 | setRootPassword: (password: string): any => http.Post('/apps/mysql/root_password', { password }) 20 | } 21 | -------------------------------------------------------------------------------- /web/src/api/apps/nginx/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 负载状态 5 | load: (): any => http.Get('/apps/nginx/load'), 6 | // 获取配置 7 | config: (): any => http.Get('/apps/nginx/config'), 8 | // 保存配置 9 | saveConfig: (config: string): any => http.Post('/apps/nginx/config', { config }), 10 | // 获取错误日志 11 | errorLog: (): any => http.Get('/apps/nginx/error_log'), 12 | // 清空错误日志 13 | clearErrorLog: (): any => http.Post('/apps/nginx/clear_error_log') 14 | } 15 | -------------------------------------------------------------------------------- /web/src/api/apps/phpmyadmin/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 获取信息 5 | info: (): any => http.Get('/apps/phpmyadmin/info'), 6 | // 设置端口 7 | port: (port: number): any => http.Post('/apps/phpmyadmin/port', { port }), 8 | // 获取配置 9 | config: (): any => http.Get('/apps/phpmyadmin/config'), 10 | // 保存配置 11 | updateConfig: (config: string): any => http.Post('/apps/phpmyadmin/config', { config }) 12 | } 13 | -------------------------------------------------------------------------------- /web/src/api/apps/podman/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 获取注册表配置 5 | registryConfig: (): any => http.Get('/apps/podman/registry_config'), 6 | // 保存注册表配置 7 | saveRegistryConfig: (config: string): any => 8 | http.Post('/apps/podman/registry_config', { config }), 9 | // 获取存储配置 10 | storageConfig: (): any => http.Get('/apps/podman/storage_config'), 11 | // 保存存储配置 12 | saveStorageConfig: (config: string): any => http.Post('/apps/podman/storage_config', { config }) 13 | } 14 | -------------------------------------------------------------------------------- /web/src/api/apps/postgresql/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 负载状态 5 | load: (): any => http.Get('/apps/postgresql/load'), 6 | // 获取配置 7 | config: (): any => http.Get('/apps/postgresql/config'), 8 | // 保存配置 9 | saveConfig: (config: string): any => http.Post('/apps/postgresql/config', { config }), 10 | // 获取用户配置 11 | userConfig: (): any => http.Get('/apps/postgresql/user_config'), 12 | // 保存配置 13 | saveUserConfig: (config: string): any => http.Post('/apps/postgresql/user_config', { config }), 14 | // 获取日志 15 | log: (): any => http.Get('/apps/postgresql/log'), 16 | // 清空错误日志 17 | clearLog: (): any => http.Post('/apps/postgresql/clear_log') 18 | } 19 | -------------------------------------------------------------------------------- /web/src/api/apps/pureftpd/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 列表 5 | list: (page: number, limit: number): any => 6 | http.Get('/apps/pureftpd/users', { params: { page, limit } }), 7 | // 添加 8 | add: (username: string, password: string, path: string): any => 9 | http.Post('/apps/pureftpd/users', { username, password, path }), 10 | // 删除 11 | delete: (username: string): any => http.Delete(`/apps/pureftpd/users/${username}`), 12 | // 修改密码 13 | changePassword: (username: string, password: string): any => 14 | http.Post(`/apps/pureftpd/users/${username}/password`, { password }), 15 | // 获取端口 16 | port: (): any => http.Get('/apps/pureftpd/port'), 17 | // 修改端口 18 | updatePort: (port: number): any => http.Post('/apps/pureftpd/port', { port }) 19 | } 20 | -------------------------------------------------------------------------------- /web/src/api/apps/redis/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 负载状态 5 | load: (): any => http.Get('/apps/redis/load'), 6 | // 获取配置 7 | config: (): any => http.Get('/apps/redis/config'), 8 | // 保存配置 9 | saveConfig: (config: string): any => http.Post('/apps/redis/config', { config }) 10 | } 11 | -------------------------------------------------------------------------------- /web/src/api/apps/rsync/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 获取配置 5 | config: (): any => http.Get('/apps/rsync/config'), 6 | // 保存配置 7 | saveConfig: (config: string): any => http.Post('/apps/rsync/config', { config }), 8 | // 模块列表 9 | modules: (page: number, limit: number): any => 10 | http.Get('/apps/rsync/modules', { params: { page, limit } }), 11 | // 添加模块 12 | addModule: (module: any): any => http.Post('/apps/rsync/modules', module), 13 | // 删除模块 14 | deleteModule: (name: string): any => http.Delete(`/apps/rsync/modules/${name}`), 15 | // 更新模块 16 | updateModule: (name: string, module: any): any => http.Post(`/apps/rsync/modules/${name}`, module) 17 | } 18 | -------------------------------------------------------------------------------- /web/src/api/apps/s3fs/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 列表 5 | mounts: (page: number, limit: number): any => 6 | http.Get('/apps/s3fs/mounts', { params: { page, limit } }), 7 | // 添加 8 | add: (data: any): any => http.Post('/apps/s3fs/mounts', data), 9 | // 删除 10 | delete: (id: number): any => http.Delete('/apps/s3fs/mounts', { id }) 11 | } 12 | -------------------------------------------------------------------------------- /web/src/api/panel/app/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 获取应用列表 5 | list: (page: number, limit: number): any => http.Get('/app/list', { params: { page, limit } }), 6 | // 安装应用 7 | install: (slug: string, channel: string | null): any => 8 | http.Post('/app/install', { slug, channel }), 9 | // 卸载应用 10 | uninstall: (slug: string): any => http.Post('/app/uninstall', { slug }), 11 | // 更新应用 12 | update: (slug: string): any => http.Post('/app/update', { slug }), 13 | // 设置首页显示 14 | updateShow: (slug: string, show: boolean): any => http.Post('/app/update_show', { slug, show }), 15 | // 应用是否已安装 16 | isInstalled: (slug: string): any => http.Get('/app/is_installed', { params: { slug } }), 17 | // 更新缓存 18 | updateCache: (): any => http.Get('/app/update_cache') 19 | } 20 | -------------------------------------------------------------------------------- /web/src/api/panel/backup/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 获取备份列表 5 | list: (type: string, page: number, limit: number): any => 6 | http.Get(`/backup/${type}`, { params: { page, limit } }), 7 | // 创建备份 8 | create: (type: string, target: string, path: string): any => 9 | http.Post(`/backup/${type}`, { target, path }), 10 | // 上传备份 11 | upload: (type: string, formData: FormData): any => http.Post(`/backup/${type}/upload`, formData), 12 | // 删除备份 13 | delete: (type: string, file: string): any => http.Delete(`/backup/${type}/delete`, { file }), 14 | // 恢复备份 15 | restore: (type: string, file: string, target: string): any => 16 | http.Post(`/backup/${type}/restore`, { file, target }) 17 | } 18 | -------------------------------------------------------------------------------- /web/src/api/panel/cron/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 获取任务列表 5 | list: (page: number, limit: number): any => http.Get('/cron', { params: { page, limit } }), 6 | // 获取任务脚本 7 | get: (id: number): any => http.Get('/cron/' + id), 8 | // 创建任务 9 | create: (task: any): any => http.Post('/cron', task), 10 | // 修改任务 11 | update: (id: number, name: string, time: string, script: string): any => 12 | http.Put('/cron/' + id, { name, time, script }), 13 | // 删除任务 14 | delete: (id: number): any => http.Delete(`/cron/${id}`), 15 | // 修改任务状态 16 | status: (id: number, status: boolean): any => http.Post('/cron/' + id + '/status', { status }) 17 | } 18 | -------------------------------------------------------------------------------- /web/src/api/panel/dashboard/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 面板信息 5 | panel: (): any => http.Get('/dashboard/panel'), 6 | // 首页应用 7 | homeApps: (): any => http.Get('/dashboard/home_apps'), 8 | // 实时信息 9 | current: (nets: string[], disks: string[]): any => 10 | http.Post('/dashboard/current', { nets, disks }, { meta: { noAlert: true } }), 11 | // 系统信息 12 | systemInfo: (): any => http.Get('/dashboard/system_info'), 13 | // 统计信息 14 | countInfo: (): any => http.Get('/dashboard/count_info'), 15 | // 已安装的数据库和PHP 16 | installedDbAndPhp: (): any => http.Get('/dashboard/installed_db_and_php'), 17 | // 检查更新 18 | checkUpdate: (): any => http.Get('/dashboard/check_update'), 19 | // 更新日志 20 | updateInfo: (): any => http.Get('/dashboard/update_info'), 21 | // 更新面板 22 | update: (): any => http.Post('/dashboard/update'), 23 | // 重启面板 24 | restart: (): any => http.Post('/dashboard/restart') 25 | } 26 | -------------------------------------------------------------------------------- /web/src/api/panel/firewall/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 获取防火墙状态 5 | status: (): any => http.Get('/firewall/status'), 6 | // 设置防火墙状态 7 | updateStatus: (status: boolean): any => http.Post('/firewall/status', { status }), 8 | // 获取防火墙规则 9 | rules: (page: number, limit: number): any => 10 | http.Get('/firewall/rule', { params: { page, limit } }), 11 | // 创建防火墙规则 12 | createRule: (rule: any): any => http.Post('/firewall/rule', rule), 13 | // 删除防火墙规则 14 | deleteRule: (rule: any): any => http.Delete('/firewall/rule', rule), 15 | // 获取防火墙IP规则 16 | ipRules: (page: number, limit: number): any => 17 | http.Get('/firewall/ip_rule', { params: { page, limit } }), 18 | // 创建防火墙IP规则 19 | createIpRule: (rule: any): any => http.Post('/firewall/ip_rule', rule), 20 | // 删除防火墙IP规则 21 | deleteIpRule: (rule: any): any => http.Delete('/firewall/ip_rule', rule), 22 | // 获取防火墙转发规则 23 | forwards: (page: number, limit: number): any => 24 | http.Get('/firewall/forward', { params: { page, limit } }), 25 | // 创建防火墙转发规则 26 | createForward: (rule: any): any => http.Post('/firewall/forward', rule), 27 | // 删除防火墙转发规则 28 | deleteForward: (rule: any): any => http.Delete('/firewall/forward', rule) 29 | } 30 | -------------------------------------------------------------------------------- /web/src/api/panel/monitor/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 开关 5 | setting: (): any => http.Get('/monitor/setting'), 6 | // 保存天数 7 | updateSetting: (enabled: boolean, days: number): any => 8 | http.Post('/monitor/setting', { enabled, days }), 9 | // 清空监控记录 10 | clear: (): any => http.Post('/monitor/clear'), 11 | // 监控记录 12 | list: (start: number, end: number): any => http.Get('/monitor/list', { params: { start, end } }) 13 | } 14 | -------------------------------------------------------------------------------- /web/src/api/panel/process/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 获取进程列表 5 | list: (page: number, limit: number) => http.Get(`/process`, { params: { page, limit } }), 6 | // 杀死进程 7 | kill: (pid: number) => http.Post(`/process/kill`, { pid }) 8 | } 9 | -------------------------------------------------------------------------------- /web/src/api/panel/safe/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | ssh: (): any => http.Get('/safe/ssh'), 5 | updateSsh: (status: boolean, port: number): any => http.Post('/safe/ssh', { status, port }), 6 | pingStatus: (): any => http.Get('/safe/ping'), 7 | updatePingStatus: (status: boolean): any => http.Post('/safe/ping', { status }) 8 | } 9 | -------------------------------------------------------------------------------- /web/src/api/panel/setting/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 获取设置 5 | list: (): any => http.Get('/setting'), 6 | // 保存设置 7 | update: (settings: any): any => http.Post('/setting', settings) 8 | } 9 | -------------------------------------------------------------------------------- /web/src/api/panel/ssh/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 获取主机列表 5 | list: (page: number, limit: number): any => http.Get('/ssh', { params: { page, limit } }), 6 | // 获取主机信息 7 | get: (id: number): any => http.Get(`/ssh/${id}`), 8 | // 创建主机 9 | create: (req: any): any => http.Post('/ssh', req), 10 | // 修改主机 11 | update: (id: number, req: any): any => http.Put(`/ssh/${id}`, req), 12 | // 删除主机 13 | delete: (id: number): any => http.Delete(`/ssh/${id}`) 14 | } 15 | -------------------------------------------------------------------------------- /web/src/api/panel/systemctl/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 服务状态 5 | status: (service: string): any => http.Get('/systemctl/status', { params: { service } }), 6 | // 是否启用服务 7 | isEnabled: (service: string): any => http.Get('/systemctl/is_enabled', { params: { service } }), 8 | // 启用服务 9 | enable: (service: string): any => http.Post('/systemctl/enable', { service }), 10 | // 禁用服务 11 | disable: (service: string): any => http.Post('/systemctl/disable', { service }), 12 | // 重启服务 13 | restart: (service: string): any => http.Post('/systemctl/restart', { service }), 14 | // 重载服务 15 | reload: (service: string): any => http.Post('/systemctl/reload', { service }), 16 | // 启动服务 17 | start: (service: string): any => http.Post('/systemctl/start', { service }), 18 | // 停止服务 19 | stop: (service: string): any => http.Post('/systemctl/stop', { service }) 20 | } 21 | -------------------------------------------------------------------------------- /web/src/api/panel/task/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 获取状态 5 | status: (): any => http.Get('/task/status'), 6 | // 获取任务列表 7 | list: (page: number, limit: number): any => http.Get('/task', { params: { page, limit } }), 8 | // 获取任务 9 | get: (id: number): any => http.Get(`/task/${id}`), 10 | // 删除任务 11 | delete: (id: number): any => http.Delete(`/task/${id}`) 12 | } 13 | -------------------------------------------------------------------------------- /web/src/api/panel/toolbox-benchmark/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/utils' 2 | 3 | export default { 4 | // 运行评分 5 | test: (name: string): any => http.Post('/toolbox_benchmark/test', { name }) 6 | } 7 | -------------------------------------------------------------------------------- /web/src/api/ws/index.ts: -------------------------------------------------------------------------------- 1 | const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws' 2 | const base = `${protocol}://${window.location.host}/api/ws` 3 | 4 | export default { 5 | // 执行命令 6 | exec: (cmd: string): Promise => { 7 | return new Promise((resolve, reject) => { 8 | const ws = new WebSocket(`${base}/exec`) 9 | ws.onopen = () => { 10 | ws.send(cmd) 11 | resolve(ws) 12 | } 13 | ws.onerror = (e) => reject(e) 14 | }) 15 | }, 16 | // 连接SSH 17 | ssh: (id: number): Promise => { 18 | return new Promise((resolve, reject) => { 19 | const ws = new WebSocket(`${base}/ssh?id=${id}`) 20 | ws.onopen = () => resolve(ws) 21 | ws.onerror = (e) => reject(e) 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /web/src/assets/images/404.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tnb-labs/panel/f3cec5e476fbe0fdbd6567ea67ca2bfa1745c258/web/src/assets/images/404.webp -------------------------------------------------------------------------------- /web/src/assets/images/login_bg.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tnb-labs/panel/f3cec5e476fbe0fdbd6567ea67ca2bfa1745c258/web/src/assets/images/login_bg.webp -------------------------------------------------------------------------------- /web/src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tnb-labs/panel/f3cec5e476fbe0fdbd6567ea67ca2bfa1745c258/web/src/assets/images/logo.png -------------------------------------------------------------------------------- /web/src/components/custom/TheIcon.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /web/src/components/page/AppPage.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /web/src/components/page/CommonPage.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {{ title ? title : route.meta.title ? translateTitle(route.meta.title) : '' }} 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /web/src/layout/AppMain.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /web/src/layout/header/IndexView.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /web/src/layout/header/components/FullScreen.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {{ $gettext('Fullscreen Display') }} 17 | 18 | 19 | -------------------------------------------------------------------------------- /web/src/layout/header/components/MenuCollapse.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {{ $gettext('Menu Zoom') }} 18 | 19 | 20 | -------------------------------------------------------------------------------- /web/src/layout/header/components/ReloadPage.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {{ $gettext('Refresh Tab') }} 21 | 22 | 23 | -------------------------------------------------------------------------------- /web/src/layout/header/components/ThemeMode.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {{ $gettext('Switch Theme') }} 18 | 19 | 20 | -------------------------------------------------------------------------------- /web/src/layout/header/components/ThemeSetting.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 18 | 19 | {{ $gettext('Set Theme Color') }} 20 | 21 | 22 | -------------------------------------------------------------------------------- /web/src/layout/sidebar/IndexView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /web/src/layout/sidebar/components/SideLogo.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | 16 | {{ themeStore.name }} 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /web/src/locales/.gitignore: -------------------------------------------------------------------------------- 1 | translations.json -------------------------------------------------------------------------------- /web/src/locales/menu.ts: -------------------------------------------------------------------------------- 1 | import { $gettext } from '@/utils' 2 | 3 | // 变通方法,由于 gettext 不能直接对动态标题进行翻译 4 | export function translateTitle(key: string): string { 5 | const titles: { [key: string]: string } = { 6 | // 主菜单标题 7 | Apps: $gettext('Apps'), 8 | Backup: $gettext('Backup'), 9 | Certificate: $gettext('Certificate'), 10 | Container: $gettext('Container'), 11 | Dashboard: $gettext('Dashboard'), 12 | Update: $gettext('Update'), 13 | Database: $gettext('Database'), 14 | Files: $gettext('Files'), 15 | Firewall: $gettext('Firewall'), 16 | Monitoring: $gettext('Monitoring'), 17 | Settings: $gettext('Settings'), 18 | Terminal: $gettext('Terminal'), 19 | Tasks: $gettext('Tasks'), 20 | Toolbox: $gettext('Toolbox'), 21 | System: $gettext('System'), 22 | Benchmark: $gettext('Benchmark'), 23 | Website: $gettext('Website'), 24 | 'Website Edit': $gettext('Website Edit'), 25 | // 应用标题 26 | 'Fail2ban Manager': $gettext('Fail2ban Manager'), 27 | 'S3fs Manager': $gettext('S3fs Manager'), 28 | 'Supervisor Manager': $gettext('Supervisor Manager'), 29 | 'Rsync Manager': $gettext('Rsync Manager'), 30 | 'Frp Manager': $gettext('Frp Manager') 31 | } 32 | 33 | return titles[key] || key 34 | } 35 | -------------------------------------------------------------------------------- /web/src/router/guard/index.ts: -------------------------------------------------------------------------------- 1 | import { createTabGuard } from '@/router/guard/tab-guard' 2 | import type { Router } from 'vue-router' 3 | import { createAppInstallGuard } from './app-install-guard' 4 | import { createPageLoadingGuard } from './page-loading-guard' 5 | import { createPageTitleGuard } from './page-title-guard' 6 | 7 | export function setupRouterGuard(router: Router) { 8 | createPageLoadingGuard(router) 9 | createPageTitleGuard(router) 10 | createTabGuard(router) 11 | createAppInstallGuard(router) 12 | } 13 | -------------------------------------------------------------------------------- /web/src/router/guard/page-loading-guard.ts: -------------------------------------------------------------------------------- 1 | import type { Router } from 'vue-router' 2 | 3 | export function createPageLoadingGuard(router: Router) { 4 | router.beforeEach(() => { 5 | window.$loadingBar?.start() 6 | }) 7 | 8 | router.afterEach(() => { 9 | setTimeout(() => { 10 | window.$loadingBar?.finish() 11 | }, 200) 12 | }) 13 | 14 | router.onError(() => { 15 | window.$loadingBar?.error() 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /web/src/router/guard/page-title-guard.ts: -------------------------------------------------------------------------------- 1 | import type { Router } from 'vue-router' 2 | 3 | import { translateTitle } from '@/locales/menu' 4 | import { useThemeStore } from '@/store' 5 | 6 | export function createPageTitleGuard(router: Router) { 7 | const themeStore = useThemeStore() 8 | router.afterEach((to) => { 9 | const pageTitle = typeof to.meta.title === 'string' ? translateTitle(to.meta.title) : '404' 10 | if (pageTitle) document.title = `${pageTitle} | ${themeStore.name}` 11 | else document.title = themeStore.name 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /web/src/router/guard/tab-guard.ts: -------------------------------------------------------------------------------- 1 | import { useTabStore } from '@/store' 2 | import type { Router } from 'vue-router' 3 | 4 | export const EXCLUDE_TAB = ['/404', '/403', '/login'] 5 | 6 | export function createTabGuard(router: Router) { 7 | router.afterEach((to) => { 8 | if (EXCLUDE_TAB.includes(to.path)) return 9 | const tabStore = useTabStore() 10 | const { name, fullPath: path } = to 11 | const title = String(to.meta?.title) 12 | tabStore.addTab({ 13 | name: String(name), 14 | path, 15 | title, 16 | keepAlive: false 17 | }) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /web/src/router/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { $gettext } from '@/utils/gettext' 2 | import type { RouteModule, RoutesType, RouteType } from '~/types/router' 3 | 4 | export const basicRoutes: RoutesType = [ 5 | { 6 | name: '404', 7 | path: '/404', 8 | component: () => import('@/views/error-page/NotFound.vue'), 9 | isHidden: true 10 | }, 11 | 12 | { 13 | name: 'Login', 14 | path: '/login', 15 | component: () => import('@/views/login/IndexView.vue'), 16 | isHidden: true, 17 | meta: { 18 | title: $gettext('Login') 19 | } 20 | } 21 | ] 22 | 23 | export const NOT_FOUND_ROUTE: RouteType = { 24 | name: 'NotFound', 25 | path: '/:pathMatch(.*)*', 26 | redirect: '/404', 27 | isHidden: true 28 | } 29 | 30 | export const EMPTY_ROUTE: RouteType = { 31 | name: 'Empty', 32 | path: '/:pathMatch(.*)*', 33 | component: () => {} 34 | } 35 | 36 | const modules = import.meta.glob('@/views/**/route.ts', { 37 | eager: true 38 | }) as RouteModule 39 | const asyncRoutes: RoutesType = [] 40 | Object.keys(modules).forEach((key) => { 41 | asyncRoutes.push(modules[key].default) 42 | }) 43 | 44 | export { asyncRoutes } 45 | -------------------------------------------------------------------------------- /web/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' 2 | import type { App } from 'vue' 3 | 4 | export async function setupStore(app: App) { 5 | const pinia = createPinia() 6 | pinia.use(piniaPluginPersistedstate) 7 | app.use(pinia) 8 | } 9 | 10 | export * from './modules' 11 | -------------------------------------------------------------------------------- /web/src/store/modules/app/index.ts: -------------------------------------------------------------------------------- 1 | export const useAppStore = defineStore('app', { 2 | state() { 3 | return {} 4 | }, 5 | actions: {} 6 | }) 7 | -------------------------------------------------------------------------------- /web/src/store/modules/file/index.ts: -------------------------------------------------------------------------------- 1 | export interface File { 2 | path: string 3 | } 4 | 5 | export const useFileStore = defineStore('file', { 6 | state: (): File => { 7 | return { 8 | path: '/' 9 | } 10 | }, 11 | actions: { 12 | set(info: File) { 13 | this.path = info.path 14 | } 15 | }, 16 | persist: true 17 | }) 18 | -------------------------------------------------------------------------------- /web/src/store/modules/index.ts: -------------------------------------------------------------------------------- 1 | export * from './file' 2 | export * from './permission' 3 | export * from './tab' 4 | export * from './theme' 5 | export * from './user' 6 | -------------------------------------------------------------------------------- /web/src/store/modules/permission/helpers.ts: -------------------------------------------------------------------------------- 1 | import type { RoutesType, RouteType } from '~/types/router' 2 | 3 | function hasPermission(route: RouteType, role: string[]) { 4 | // * 不需要权限直接返回true 5 | if (!route.meta?.requireAuth) return true 6 | 7 | const routeRole = route.meta?.role ? route.meta.role : [] 8 | 9 | // * 登录用户没有角色或者路由没有设置角色判定为没有权限 10 | if (!role.length || !routeRole.length) return false 11 | 12 | // * 路由指定的角色包含任一登录用户角色则判定有权限 13 | return role.some((item) => routeRole.includes(item)) 14 | } 15 | 16 | export function filterAsyncRoutes(routes: RoutesType = [], role: Array): RoutesType { 17 | const ret: RoutesType = [] 18 | routes.forEach((route) => { 19 | if (hasPermission(route, role)) { 20 | const curRoute: RouteType = { 21 | ...route, 22 | children: [] 23 | } 24 | if (route.children && route.children.length) 25 | curRoute.children = filterAsyncRoutes(route.children, role) || [] 26 | else Reflect.deleteProperty(curRoute, 'children') 27 | 28 | ret.push(curRoute) 29 | } 30 | }) 31 | return ret 32 | } 33 | -------------------------------------------------------------------------------- /web/src/store/modules/user/index.ts: -------------------------------------------------------------------------------- 1 | import { resetRouter } from '@/router' 2 | import { usePermissionStore, useTabStore } from '@/store' 3 | import { toLogin } from '@/utils' 4 | 5 | export interface UserInfo { 6 | id?: string 7 | username?: string 8 | role?: Array 9 | } 10 | 11 | export const useUserStore = defineStore('user', { 12 | state: (): UserInfo => { 13 | return { 14 | id: '', 15 | username: '', 16 | role: [] 17 | } 18 | }, 19 | actions: { 20 | set(info: UserInfo) { 21 | this.id = info.id 22 | this.username = info.username 23 | this.role = info.role 24 | }, 25 | logout() { 26 | const { resetTabs } = useTabStore() 27 | const { resetPermission } = usePermissionStore() 28 | resetPermission() 29 | resetTabs() 30 | resetRouter() 31 | this.$reset() 32 | toLogin() 33 | } 34 | }, 35 | persist: true 36 | }) 37 | -------------------------------------------------------------------------------- /web/src/styles/reset.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | } 4 | 5 | *, 6 | ::before, 7 | ::after { 8 | margin: 0; 9 | padding: 0; 10 | box-sizing: inherit; 11 | } 12 | 13 | a { 14 | text-decoration: none; 15 | color: inherit; 16 | } 17 | 18 | a:hover, 19 | a:link, 20 | a:visited, 21 | a:active { 22 | text-decoration: none; 23 | } 24 | 25 | body { 26 | font-size: 14px; 27 | font-weight: 400; 28 | } 29 | -------------------------------------------------------------------------------- /web/src/utils/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from './router' 2 | -------------------------------------------------------------------------------- /web/src/utils/auth/router.ts: -------------------------------------------------------------------------------- 1 | import { router } from '@/router' 2 | 3 | export function toLogin() { 4 | const currentRoute = unref(router.currentRoute) 5 | const needRedirect = 6 | !currentRoute.meta.requireAuth && !['/404', '/login'].includes(router.currentRoute.value.path) 7 | router.replace({ 8 | path: '/login', 9 | query: needRedirect ? { ...currentRoute.query, redirect: currentRoute.path } : {} 10 | }) 11 | } 12 | 13 | export function toFourZeroFour() { 14 | router.replace({ 15 | path: '/404' 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /web/src/utils/common/base64.ts: -------------------------------------------------------------------------------- 1 | export function decodeBase64(base64: string): string { 2 | const binary = atob(base64) 3 | const bytes = new Uint8Array(binary.length) 4 | for (let i = 0; i < binary.length; i++) { 5 | bytes[i] = binary.charCodeAt(i) 6 | } 7 | return new TextDecoder('utf-8').decode(bytes) 8 | } 9 | 10 | export function encodeBase64(str: string): string { 11 | const bytes = new TextEncoder().encode(str) 12 | let binary = '' 13 | for (let i = 0; i < bytes.length; i++) { 14 | binary += String.fromCharCode(bytes[i]) 15 | } 16 | return btoa(binary) 17 | } 18 | -------------------------------------------------------------------------------- /web/src/utils/common/icon.ts: -------------------------------------------------------------------------------- 1 | import { addAPIProvider, Icon } from '@iconify/vue' 2 | import { NIcon } from 'naive-ui' 3 | 4 | addAPIProvider('', { 5 | resources: ['https://iconify.cdn.haozi.net'] 6 | }) 7 | 8 | interface Props { 9 | size?: number 10 | color?: string 11 | class?: string 12 | } 13 | 14 | export function renderIcon(icon: string, props: Props = { size: 12 }) { 15 | return () => h(NIcon, props, { default: () => h(Icon, { icon }) }) 16 | } 17 | -------------------------------------------------------------------------------- /web/src/utils/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base64' 2 | export * from './color' 3 | export * from './common' 4 | export * from './icon' 5 | export * from './is' 6 | export * from './naiveTools' 7 | -------------------------------------------------------------------------------- /web/src/utils/common/naiveTools.ts: -------------------------------------------------------------------------------- 1 | import { useThemeStore } from '@/store' 2 | import mitt from 'mitt' 3 | import * as NaiveUI from 'naive-ui' 4 | 5 | export async function setupNaiveDiscreteApi() { 6 | const themeStore = useThemeStore() 7 | const configProviderProps = computed(() => ({ 8 | theme: themeStore.naiveTheme, 9 | themeOverrides: themeStore.naiveThemeOverrides 10 | })) 11 | const { message, dialog, notification, loadingBar } = NaiveUI.createDiscreteApi( 12 | ['message', 'dialog', 'notification', 'loadingBar'], 13 | { configProviderProps } 14 | ) 15 | 16 | window.$loadingBar = loadingBar 17 | window.$notification = notification 18 | window.$message = message 19 | window.$dialog = dialog 20 | window.$bus = mitt() 21 | } 22 | -------------------------------------------------------------------------------- /web/src/utils/encrypt/index.ts: -------------------------------------------------------------------------------- 1 | export * from './rsa' 2 | -------------------------------------------------------------------------------- /web/src/utils/encrypt/rsa.ts: -------------------------------------------------------------------------------- 1 | import * as forge from 'node-forge' 2 | 3 | export function rsaEncrypt(data: string, publicKey: string) { 4 | const pk = forge.pki.publicKeyFromPem(publicKey) 5 | const encryptedBytes = pk.encrypt(data, 'RSA-OAEP', { 6 | md: forge.md.sha512.create() 7 | }) 8 | return forge.util.encode64(encryptedBytes) 9 | } 10 | 11 | export function rsaDecrypt(data: string, privateKey: string) { 12 | const pk = forge.pki.privateKeyFromPem(privateKey) 13 | return pk.decrypt(forge.util.decode64(data), 'RSA-OAEP', { 14 | md: forge.md.sha512.create() 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /web/src/utils/gettext/index.ts: -------------------------------------------------------------------------------- 1 | import { createGettext as vue3Gettext } from 'vue3-gettext' 2 | 3 | import translations from '@/locales/translations.json' 4 | 5 | export const locales = { 6 | en: 'English', 7 | zh_CN: '简体中文', 8 | zh_TW: '繁體中文' 9 | } 10 | 11 | export const gettext: any = vue3Gettext({ 12 | availableLanguages: locales, 13 | defaultLanguage: 'zh_CN', 14 | translations: translations, 15 | silent: true 16 | }) 17 | 18 | export function $gettext(msgid: string, params?: Record) { 19 | return gettext.$gettext(msgid, params) 20 | } 21 | 22 | export function $ngettext( 23 | msgid: string, 24 | plural: string, 25 | n: number, 26 | params?: Record 27 | ) { 28 | return gettext.$ngettext(msgid, plural, n, params) 29 | } 30 | 31 | export function setCurrent(language: string) { 32 | gettext.current = language 33 | } 34 | -------------------------------------------------------------------------------- /web/src/utils/http/helpers.ts: -------------------------------------------------------------------------------- 1 | import { useUserStore } from '@/store' 2 | 3 | export function resolveResError(code: number | string | undefined, msg = ''): string { 4 | switch (code) { 5 | case 400: 6 | case 422: 7 | msg = msg ?? '请求参数错误' 8 | break 9 | case 401: 10 | msg = msg ?? '登录已过期' 11 | useUserStore().logout() 12 | break 13 | case 403: 14 | msg = msg ?? '没有权限' 15 | break 16 | case 404: 17 | msg = msg ?? '资源或接口不存在' 18 | break 19 | case 500: 20 | msg = msg ?? '服务器异常' 21 | break 22 | default: 23 | msg = msg ?? `【${code}】: 未知异常!` 24 | break 25 | } 26 | return msg 27 | } 28 | -------------------------------------------------------------------------------- /web/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { translateTitle } from '@/locales/menu' 2 | export * from './auth' 3 | export * from './common' 4 | export * from './file' 5 | export * from './gettext' 6 | export * from './http' 7 | export * from './storage' 8 | -------------------------------------------------------------------------------- /web/src/utils/storage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './local' 2 | -------------------------------------------------------------------------------- /web/src/views/app/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'app', 7 | path: '/app', 8 | component: Layout, 9 | meta: { 10 | order: 90 11 | }, 12 | children: [ 13 | { 14 | name: 'app-index', 15 | path: '', 16 | component: () => import('./IndexView.vue'), 17 | meta: { 18 | title: 'Apps', 19 | icon: 'mdi:apps', 20 | role: ['admin'], 21 | requireAuth: true 22 | } 23 | } 24 | ] 25 | } as RouteType 26 | -------------------------------------------------------------------------------- /web/src/views/app/types.ts: -------------------------------------------------------------------------------- 1 | export interface App { 2 | name: string 3 | description: string 4 | slug: string 5 | channels: Channel[] 6 | installed: boolean 7 | installed_channel: string 8 | installed_version: string 9 | update_exist: boolean 10 | show: boolean 11 | } 12 | 13 | export interface Channel { 14 | slug: string 15 | name: string 16 | panel: string 17 | subs: Sub[] 18 | } 19 | 20 | export interface Sub { 21 | log: string 22 | version: string 23 | } 24 | -------------------------------------------------------------------------------- /web/src/views/apps/codeserver/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'codeserver', 7 | path: '/apps/codeserver', 8 | component: Layout, 9 | isHidden: true, 10 | children: [ 11 | { 12 | name: 'apps-codeserver-index', 13 | path: '', 14 | component: () => import('./IndexView.vue'), 15 | meta: { 16 | title: 'Code Server', 17 | icon: 'simple-icons:coder', 18 | role: ['admin'], 19 | requireAuth: true 20 | } 21 | } 22 | ] 23 | } as RouteType 24 | -------------------------------------------------------------------------------- /web/src/views/apps/docker/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'docker', 7 | path: '/apps/docker', 8 | component: Layout, 9 | isHidden: true, 10 | children: [ 11 | { 12 | name: 'apps-docker-index', 13 | path: '', 14 | component: () => import('./IndexView.vue'), 15 | meta: { 16 | title: 'Docker', 17 | icon: 'logos:docker-icon', 18 | role: ['admin'], 19 | requireAuth: true 20 | } 21 | } 22 | ] 23 | } as RouteType 24 | -------------------------------------------------------------------------------- /web/src/views/apps/fail2ban/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'fail2ban', 7 | path: '/apps/fail2ban', 8 | component: Layout, 9 | isHidden: true, 10 | children: [ 11 | { 12 | name: 'apps-fail2ban-index', 13 | path: '', 14 | component: () => import('./IndexView.vue'), 15 | meta: { 16 | title: 'Fail2ban Manager', 17 | icon: 'mdi:wall-fire', 18 | role: ['admin'], 19 | requireAuth: true 20 | } 21 | } 22 | ] 23 | } as RouteType 24 | -------------------------------------------------------------------------------- /web/src/views/apps/frp/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'frp', 7 | path: '/apps/frp', 8 | component: Layout, 9 | isHidden: true, 10 | children: [ 11 | { 12 | name: 'apps-frp-index', 13 | path: '', 14 | component: () => import('./IndexView.vue'), 15 | meta: { 16 | title: 'Frp Manager', 17 | icon: 'icon-park-outline:connection-box', 18 | role: ['admin'], 19 | requireAuth: true 20 | } 21 | } 22 | ] 23 | } as RouteType 24 | -------------------------------------------------------------------------------- /web/src/views/apps/gitea/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'gitea', 7 | path: '/apps/gitea', 8 | component: Layout, 9 | isHidden: true, 10 | children: [ 11 | { 12 | name: 'apps-gitea-index', 13 | path: '', 14 | component: () => import('./IndexView.vue'), 15 | meta: { 16 | title: 'Gitea', 17 | icon: 'simple-icons:gitea', 18 | role: ['admin'], 19 | requireAuth: true 20 | } 21 | } 22 | ] 23 | } as RouteType 24 | -------------------------------------------------------------------------------- /web/src/views/apps/memcached/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'memcached', 7 | path: '/apps/memcached', 8 | component: Layout, 9 | isHidden: true, 10 | children: [ 11 | { 12 | name: 'apps-memcached-index', 13 | path: '', 14 | component: () => import('./IndexView.vue'), 15 | meta: { 16 | title: 'Memcached', 17 | icon: 'logos:memcached', 18 | role: ['admin'], 19 | requireAuth: true 20 | } 21 | } 22 | ] 23 | } as RouteType 24 | -------------------------------------------------------------------------------- /web/src/views/apps/minio/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'minio', 7 | path: '/apps/minio', 8 | component: Layout, 9 | isHidden: true, 10 | children: [ 11 | { 12 | name: 'apps-minio-index', 13 | path: '', 14 | component: () => import('./IndexView.vue'), 15 | meta: { 16 | title: 'Minio', 17 | icon: 'simple-icons:minio', 18 | role: ['admin'], 19 | requireAuth: true 20 | } 21 | } 22 | ] 23 | } as RouteType 24 | -------------------------------------------------------------------------------- /web/src/views/apps/mysql/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'mysql', 7 | path: '/apps/mysql', 8 | component: Layout, 9 | isHidden: true, 10 | children: [ 11 | { 12 | name: 'apps-mysql-index', 13 | path: '', 14 | component: () => import('./IndexView.vue'), 15 | meta: { 16 | title: 'Percona (MySQL)', 17 | icon: 'logos:percona', 18 | role: ['admin'], 19 | requireAuth: true 20 | } 21 | } 22 | ] 23 | } as RouteType 24 | -------------------------------------------------------------------------------- /web/src/views/apps/nginx/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'nginx', 7 | path: '/apps/nginx', 8 | component: Layout, 9 | isHidden: true, 10 | children: [ 11 | { 12 | name: 'apps-nginx-index', 13 | path: '', 14 | component: () => import('./IndexView.vue'), 15 | meta: { 16 | title: 'OpenResty (Nginx)', 17 | icon: 'logos:nginx', 18 | role: ['admin'], 19 | requireAuth: true 20 | } 21 | } 22 | ] 23 | } as RouteType 24 | -------------------------------------------------------------------------------- /web/src/views/apps/nginx/types.ts: -------------------------------------------------------------------------------- 1 | export interface Task { 2 | id: number 3 | name: string 4 | status: string 5 | shell: string 6 | log: string 7 | created_at: string 8 | updated_at: string 9 | } 10 | -------------------------------------------------------------------------------- /web/src/views/apps/php74/IndexView.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /web/src/views/apps/php74/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'php74', 7 | path: '/apps/php74', 8 | component: Layout, 9 | isHidden: true, 10 | children: [ 11 | { 12 | name: 'apps-php74-index', 13 | path: '', 14 | component: () => import('./IndexView.vue'), 15 | meta: { 16 | title: 'PHP 7.4', 17 | icon: 'logos:php', 18 | role: ['admin'], 19 | requireAuth: true 20 | } 21 | } 22 | ] 23 | } as RouteType 24 | -------------------------------------------------------------------------------- /web/src/views/apps/php80/IndexView.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /web/src/views/apps/php80/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'php80', 7 | path: '/apps/php80', 8 | component: Layout, 9 | isHidden: true, 10 | children: [ 11 | { 12 | name: 'apps-php80-index', 13 | path: '', 14 | component: () => import('./IndexView.vue'), 15 | meta: { 16 | title: 'PHP 8.0', 17 | icon: 'logos:php', 18 | role: ['admin'], 19 | requireAuth: true 20 | } 21 | } 22 | ] 23 | } as RouteType 24 | -------------------------------------------------------------------------------- /web/src/views/apps/php81/IndexView.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /web/src/views/apps/php81/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'php81', 7 | path: '/apps/php81', 8 | component: Layout, 9 | isHidden: true, 10 | children: [ 11 | { 12 | name: 'apps-php81-index', 13 | path: '', 14 | component: () => import('./IndexView.vue'), 15 | meta: { 16 | title: 'PHP 8.1', 17 | icon: 'logos:php', 18 | role: ['admin'], 19 | requireAuth: true 20 | } 21 | } 22 | ] 23 | } as RouteType 24 | -------------------------------------------------------------------------------- /web/src/views/apps/php82/IndexView.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /web/src/views/apps/php82/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'php82', 7 | path: '/apps/php82', 8 | component: Layout, 9 | isHidden: true, 10 | children: [ 11 | { 12 | name: 'apps-php82-index', 13 | path: '', 14 | component: () => import('./IndexView.vue'), 15 | meta: { 16 | title: 'PHP 8.2', 17 | icon: 'logos:php', 18 | role: ['admin'], 19 | requireAuth: true 20 | } 21 | } 22 | ] 23 | } as RouteType 24 | -------------------------------------------------------------------------------- /web/src/views/apps/php83/IndexView.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /web/src/views/apps/php83/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'php83', 7 | path: '/apps/php83', 8 | component: Layout, 9 | isHidden: true, 10 | children: [ 11 | { 12 | name: 'apps-php83-index', 13 | path: '', 14 | component: () => import('./IndexView.vue'), 15 | meta: { 16 | title: 'PHP 8.3', 17 | icon: 'logos:php', 18 | role: ['admin'], 19 | requireAuth: true 20 | } 21 | } 22 | ] 23 | } as RouteType 24 | -------------------------------------------------------------------------------- /web/src/views/apps/php84/IndexView.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /web/src/views/apps/php84/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'php84', 7 | path: '/apps/php84', 8 | component: Layout, 9 | isHidden: true, 10 | children: [ 11 | { 12 | name: 'apps-php84-index', 13 | path: '', 14 | component: () => import('./IndexView.vue'), 15 | meta: { 16 | title: 'PHP 8.4', 17 | icon: 'logos:php', 18 | role: ['admin'], 19 | requireAuth: true 20 | } 21 | } 22 | ] 23 | } as RouteType 24 | -------------------------------------------------------------------------------- /web/src/views/apps/phpmyadmin/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'phpmyadmin', 7 | path: '/apps/phpmyadmin', 8 | component: Layout, 9 | isHidden: true, 10 | children: [ 11 | { 12 | name: 'apps-phpmyadmin-index', 13 | path: '', 14 | component: () => import('./IndexView.vue'), 15 | meta: { 16 | title: 'phpMyAdmin', 17 | icon: 'simple-icons:phpmyadmin', 18 | role: ['admin'], 19 | requireAuth: true 20 | } 21 | } 22 | ] 23 | } as RouteType 24 | -------------------------------------------------------------------------------- /web/src/views/apps/podman/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'podman', 7 | path: '/apps/podman', 8 | component: Layout, 9 | isHidden: true, 10 | children: [ 11 | { 12 | name: 'apps-podman-index', 13 | path: '', 14 | component: () => import('./IndexView.vue'), 15 | meta: { 16 | title: 'Podman', 17 | icon: 'devicon:podman', 18 | role: ['admin'], 19 | requireAuth: true 20 | } 21 | } 22 | ] 23 | } as RouteType 24 | -------------------------------------------------------------------------------- /web/src/views/apps/postgresql/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'postgresql', 7 | path: '/apps/postgresql', 8 | component: Layout, 9 | isHidden: true, 10 | children: [ 11 | { 12 | name: 'apps-postgresql-index', 13 | path: '', 14 | component: () => import('./IndexView.vue'), 15 | meta: { 16 | title: 'PostgreSQL', 17 | icon: 'logos:postgresql', 18 | role: ['admin'], 19 | requireAuth: true 20 | } 21 | } 22 | ] 23 | } as RouteType 24 | -------------------------------------------------------------------------------- /web/src/views/apps/pureftpd/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'pureftpd', 7 | path: '/apps/pureftpd', 8 | component: Layout, 9 | isHidden: true, 10 | children: [ 11 | { 12 | name: 'apps-pureftpd-index', 13 | path: '', 14 | component: () => import('./IndexView.vue'), 15 | meta: { 16 | title: 'Pure-FTPd', 17 | icon: 'mdi:server-network', 18 | role: ['admin'], 19 | requireAuth: true 20 | } 21 | } 22 | ] 23 | } as RouteType 24 | -------------------------------------------------------------------------------- /web/src/views/apps/redis/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'redis', 7 | path: '/apps/redis', 8 | component: Layout, 9 | isHidden: true, 10 | children: [ 11 | { 12 | name: 'apps-redis-index', 13 | path: '', 14 | component: () => import('./IndexView.vue'), 15 | meta: { 16 | title: 'Redis', 17 | icon: 'logos:redis', 18 | role: ['admin'], 19 | requireAuth: true 20 | } 21 | } 22 | ] 23 | } as RouteType 24 | -------------------------------------------------------------------------------- /web/src/views/apps/rsync/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'rsync', 7 | path: '/apps/rsync', 8 | component: Layout, 9 | isHidden: true, 10 | children: [ 11 | { 12 | name: 'apps-rsync-index', 13 | path: '', 14 | component: () => import('./IndexView.vue'), 15 | meta: { 16 | title: 'Rsync Manager', 17 | icon: 'file-icons:rsync', 18 | role: ['admin'], 19 | requireAuth: true 20 | } 21 | } 22 | ] 23 | } as RouteType 24 | -------------------------------------------------------------------------------- /web/src/views/apps/s3fs/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 's3fs', 7 | path: '/apps/s3fs', 8 | component: Layout, 9 | isHidden: true, 10 | children: [ 11 | { 12 | name: 'apps-s3fs-index', 13 | path: '', 14 | component: () => import('./IndexView.vue'), 15 | meta: { 16 | title: 'S3fs Manager', 17 | icon: 'logos:aws', 18 | role: ['admin'], 19 | requireAuth: true 20 | } 21 | } 22 | ] 23 | } as RouteType 24 | -------------------------------------------------------------------------------- /web/src/views/apps/supervisor/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'supervisor', 7 | path: '/apps/supervisor', 8 | component: Layout, 9 | isHidden: true, 10 | children: [ 11 | { 12 | name: 'apps-supervisor-index', 13 | path: '', 14 | component: () => import('./IndexView.vue'), 15 | meta: { 16 | title: 'Supervisor Manager', 17 | icon: 'mdi:monitor-dashboard', 18 | role: ['admin'], 19 | requireAuth: true 20 | } 21 | } 22 | ] 23 | } as RouteType 24 | -------------------------------------------------------------------------------- /web/src/views/backup/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'backup', 7 | path: '/backup', 8 | component: Layout, 9 | meta: { 10 | order: 60 11 | }, 12 | children: [ 13 | { 14 | name: 'backup-index', 15 | path: '', 16 | component: () => import('./IndexView.vue'), 17 | meta: { 18 | title: 'Backup', 19 | icon: 'mdi:backup-outline', 20 | role: ['admin'], 21 | requireAuth: true 22 | } 23 | } 24 | ] 25 | } as RouteType 26 | -------------------------------------------------------------------------------- /web/src/views/cert/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'cert', 7 | path: '/cert', 8 | component: Layout, 9 | meta: { 10 | order: 10 11 | }, 12 | children: [ 13 | { 14 | name: 'cert-index', 15 | path: '', 16 | component: () => import('./IndexView.vue'), 17 | meta: { 18 | title: 'Certificate', 19 | icon: 'mdi:certificate-outline', 20 | role: ['admin'], 21 | requireAuth: true 22 | } 23 | } 24 | ] 25 | } as RouteType 26 | -------------------------------------------------------------------------------- /web/src/views/container/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'container', 7 | path: '/container', 8 | component: Layout, 9 | meta: { 10 | order: 40 11 | }, 12 | children: [ 13 | { 14 | name: 'container-index', 15 | path: '', 16 | component: () => import('./IndexView.vue'), 17 | meta: { 18 | title: 'Container', 19 | icon: 'mdi:layers-outline', 20 | role: ['admin'], 21 | requireAuth: true 22 | } 23 | } 24 | ] 25 | } as RouteType 26 | -------------------------------------------------------------------------------- /web/src/views/dashboard/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'dashboard', 7 | path: '/', 8 | component: Layout, 9 | redirect: '/dashboard', 10 | meta: { 11 | order: 0 12 | }, 13 | children: [ 14 | { 15 | name: 'dashboard-index', 16 | path: 'dashboard', 17 | component: () => import('./IndexView.vue'), 18 | meta: { 19 | title: 'Dashboard', 20 | icon: 'mdi:gauge', 21 | role: ['admin'], 22 | requireAuth: true 23 | } 24 | }, 25 | { 26 | name: 'dashboard-update', 27 | path: 'update', 28 | component: () => import('./UpdateView.vue'), 29 | isHidden: true, 30 | meta: { 31 | title: 'Update', 32 | icon: 'mdi:archive-arrow-up-outline', 33 | role: ['admin'], 34 | requireAuth: true 35 | } 36 | } 37 | ] 38 | } as RouteType 39 | -------------------------------------------------------------------------------- /web/src/views/database/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'database', 7 | path: '/database', 8 | component: Layout, 9 | meta: { 10 | order: 2 11 | }, 12 | children: [ 13 | { 14 | name: 'database-index', 15 | path: '', 16 | component: () => import('./IndexView.vue'), 17 | meta: { 18 | title: 'Database', 19 | icon: 'mdi:database', 20 | role: ['admin'], 21 | requireAuth: true 22 | } 23 | } 24 | ] 25 | } as RouteType 26 | -------------------------------------------------------------------------------- /web/src/views/error-page/NotFound.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | {{ $gettext('Back to Home') }} 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /web/src/views/file/EditModal.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 28 | 29 | 30 | {{ $gettext('Refresh') }} 31 | {{ $gettext('Save') }} 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /web/src/views/file/PreviewModal.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /web/src/views/file/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'file', 7 | path: '/file', 8 | component: Layout, 9 | meta: { 10 | order: 50 11 | }, 12 | children: [ 13 | { 14 | name: 'file-index', 15 | path: '', 16 | component: () => import('./IndexView.vue'), 17 | meta: { 18 | title: 'Files', 19 | icon: 'mdi:folder-open-outline', 20 | role: ['admin'], 21 | requireAuth: true 22 | } 23 | } 24 | ] 25 | } as RouteType 26 | -------------------------------------------------------------------------------- /web/src/views/file/types.ts: -------------------------------------------------------------------------------- 1 | export interface Marked { 2 | name: string 3 | source: string 4 | force: boolean 5 | } 6 | -------------------------------------------------------------------------------- /web/src/views/firewall/IndexView.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /web/src/views/firewall/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'firewall', 7 | path: '/firewall', 8 | component: Layout, 9 | meta: { 10 | order: 30 11 | }, 12 | children: [ 13 | { 14 | name: 'firewall-index', 15 | path: '', 16 | component: () => import('./IndexView.vue'), 17 | meta: { 18 | title: 'Firewall', 19 | icon: 'mdi:firewall', 20 | role: ['admin'], 21 | requireAuth: true 22 | } 23 | } 24 | ] 25 | } as RouteType 26 | -------------------------------------------------------------------------------- /web/src/views/monitor/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'monitor', 7 | path: '/monitor', 8 | component: Layout, 9 | meta: { 10 | order: 20 11 | }, 12 | children: [ 13 | { 14 | name: 'monitor-index', 15 | path: '', 16 | component: () => import('./IndexView.vue'), 17 | meta: { 18 | title: 'Monitoring', 19 | icon: 'mdi:chart-line', 20 | role: ['admin'], 21 | requireAuth: true 22 | } 23 | } 24 | ] 25 | } as RouteType 26 | -------------------------------------------------------------------------------- /web/src/views/setting/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'setting', 7 | path: '/setting', 8 | component: Layout, 9 | meta: { 10 | order: 999 11 | }, 12 | children: [ 13 | { 14 | name: 'setting-index', 15 | path: '', 16 | component: () => import('./IndexView.vue'), 17 | meta: { 18 | title: 'Settings', 19 | icon: 'mdi:settings-outline', 20 | role: ['admin'], 21 | requireAuth: true 22 | } 23 | } 24 | ] 25 | } as RouteType 26 | -------------------------------------------------------------------------------- /web/src/views/setting/types.ts: -------------------------------------------------------------------------------- 1 | export interface Setting { 2 | name: string 3 | locale: string 4 | username: string 5 | password: string 6 | email: string 7 | port: number 8 | entrance: string 9 | offline_mode: boolean 10 | auto_update: boolean 11 | website_path: string 12 | backup_path: string 13 | https: boolean 14 | cert: string 15 | key: string 16 | } 17 | -------------------------------------------------------------------------------- /web/src/views/ssh/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'ssh', 7 | path: '/ssh', 8 | component: Layout, 9 | meta: { 10 | order: 70 11 | }, 12 | children: [ 13 | { 14 | name: 'ssh-index', 15 | path: '', 16 | component: () => import('./IndexView.vue'), 17 | meta: { 18 | title: 'Terminal', 19 | icon: 'mdi:console', 20 | role: ['admin'], 21 | requireAuth: true 22 | } 23 | } 24 | ] 25 | } as RouteType 26 | -------------------------------------------------------------------------------- /web/src/views/task/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'task', 7 | path: '/task', 8 | component: Layout, 9 | meta: { 10 | order: 80 11 | }, 12 | children: [ 13 | { 14 | name: 'task-index', 15 | path: '', 16 | component: () => import('./IndexView.vue'), 17 | meta: { 18 | title: 'Tasks', 19 | icon: 'mdi:timetable', 20 | role: ['admin'], 21 | requireAuth: true 22 | } 23 | } 24 | ] 25 | } as RouteType 26 | -------------------------------------------------------------------------------- /web/src/views/toolbox/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'toolbox', 7 | path: '/toolbox', 8 | component: Layout, 9 | meta: { 10 | title: 'Toolbox', 11 | icon: 'mdi:tools', 12 | order: 90 13 | }, 14 | children: [ 15 | { 16 | name: 'toolbox-system', 17 | path: 'system', 18 | component: () => import('./SystemView.vue'), 19 | meta: { 20 | title: 'System', 21 | role: ['admin'], 22 | requireAuth: true 23 | } 24 | }, 25 | { 26 | name: 'toolbox-benchmark', 27 | path: 'benchmark', 28 | component: () => import('./BenchmarkView.vue'), 29 | meta: { 30 | title: 'Benchmark', 31 | role: ['admin'], 32 | requireAuth: true 33 | } 34 | } 35 | ] 36 | } as RouteType 37 | -------------------------------------------------------------------------------- /web/src/views/website/route.ts: -------------------------------------------------------------------------------- 1 | import type { RouteType } from '~/types/router' 2 | 3 | const Layout = () => import('@/layout/IndexView.vue') 4 | 5 | export default { 6 | name: 'website', 7 | path: '/website', 8 | component: Layout, 9 | meta: { 10 | order: 1 11 | }, 12 | children: [ 13 | { 14 | name: 'website-index', 15 | path: '', 16 | component: () => import('./IndexView.vue'), 17 | meta: { 18 | title: 'Website', 19 | icon: 'mdi:web', 20 | role: ['admin'], 21 | requireAuth: true 22 | } 23 | }, 24 | { 25 | name: 'website-edit', 26 | path: 'edit/:id', 27 | component: () => import('./EditView.vue'), 28 | isHidden: true, 29 | meta: { 30 | title: 'Website Edit', 31 | icon: 'mdi:web', 32 | role: ['admin'], 33 | requireAuth: true 34 | } 35 | } 36 | ] 37 | } as RouteType 38 | -------------------------------------------------------------------------------- /web/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "noEmit": true, 6 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 7 | "target": "ESNext", 8 | "lib": [ 9 | "DOM", 10 | "ESNext" 11 | ], 12 | "baseUrl": ".", 13 | "module": "ESNext", 14 | "moduleResolution": "node", 15 | "paths": { 16 | "~/*": [ 17 | "./*" 18 | ], 19 | "@/*": [ 20 | "./src/*" 21 | ] 22 | }, 23 | "resolveJsonModule": true, 24 | "types": [ 25 | "node", 26 | "vite/client", 27 | "unplugin-icons/types/vue" 28 | ], 29 | "strict": true, 30 | "strictNullChecks": true, 31 | "noUnusedLocals": true, 32 | "esModuleInterop": true, 33 | "forceConsistentCasingInFileNames": true 34 | }, 35 | "include": [ 36 | "env.d.ts", 37 | "src/**/*", 38 | "types/**/*.d.ts", 39 | "src/**/*.vue" 40 | ], 41 | "exclude": [ 42 | "src/**/__tests__/*" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@tsconfig/node22/tsconfig.json" 4 | ], 5 | "compilerOptions": { 6 | "composite": true, 7 | "noEmit": true, 8 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 9 | "module": "ESNext", 10 | "moduleResolution": "Bundler", 11 | "paths": { 12 | "~/*": [ 13 | "./*" 14 | ], 15 | "@/*": [ 16 | "./src/*" 17 | ] 18 | }, 19 | "types": [ 20 | "node" 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /web/types/env.d.ts: -------------------------------------------------------------------------------- 1 | type ProxyType = 'dev' | 'test' | 'prod' 2 | 3 | interface ViteEnv { 4 | VITE_PORT: number 5 | VITE_USE_PROXY?: boolean 6 | VITE_USE_HASH?: boolean 7 | VITE_APP_TITLE: string 8 | VITE_PUBLIC_PATH: string 9 | VITE_BASE_API: string 10 | VITE_PROXY_TYPE?: ProxyType 11 | } 12 | 13 | interface ProxyConfig { 14 | /** 匹配代理的前缀,接口地址匹配到此前缀将代理的target地址 */ 15 | prefix: string 16 | /** 代理目标地址,后端真实接口地址 */ 17 | target: string 18 | /** 是否校验https证书 */ 19 | secure?: boolean 20 | /** 是否修改请求头中的host */ 21 | changeOrigin?: boolean 22 | /** 是否代理websocket */ 23 | ws?: boolean 24 | } 25 | -------------------------------------------------------------------------------- /web/types/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | $loadingBar: import('naive-ui').LoadingBarProviderInst 3 | $dialog: import('naive-ui').DialogProviderInst 4 | $message: import('naive-ui').MessageProviderInst 5 | $notification: import('naive-ui').NotificationProviderInst 6 | $bus: import('mitt').Emitter 7 | } 8 | -------------------------------------------------------------------------------- /web/types/router.d.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | 3 | interface Meta { 4 | title?: string 5 | icon?: string 6 | order?: number 7 | role?: Array 8 | requireAuth?: boolean 9 | } 10 | 11 | interface RouteItem { 12 | name: string 13 | path: string 14 | redirect?: string 15 | isHidden?: boolean 16 | meta?: Meta 17 | children?: RoutesType 18 | } 19 | 20 | type RouteType = RouteRecordRaw & RouteItem 21 | 22 | type RoutesType = Array 23 | 24 | /** 前端导入的路由模块 */ 25 | type RouteModule = Record 26 | -------------------------------------------------------------------------------- /web/types/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue' 3 | const component: DefineComponent<{}, {}, any> 4 | export default component 5 | } 6 | -------------------------------------------------------------------------------- /web/types/theme.d.ts: -------------------------------------------------------------------------------- 1 | /** 侧边栏 */ 2 | interface Sider { 3 | width: number 4 | /** 折叠时的宽度 */ 5 | collapsedWidth: number 6 | /** 是否折叠 */ 7 | collapsed: boolean 8 | } 9 | 10 | /** 头部样式 */ 11 | interface Header { 12 | /** 是否显示 */ 13 | visible: boolean 14 | /** 头部高度 */ 15 | height: number 16 | } 17 | 18 | /** 标多页签样式 */ 19 | interface Tab { 20 | /** 是否显示 */ 21 | visible: boolean 22 | /** 头部高度 */ 23 | height: number 24 | } 25 | 26 | interface OtherColor { 27 | /** 信息 */ 28 | info: string 29 | /** 成功 */ 30 | success: string 31 | /** 警告 */ 32 | warning: string 33 | /** 错误 */ 34 | error: string 35 | } 36 | 37 | declare namespace Theme { 38 | interface Setting { 39 | isMobile: boolean 40 | darkMode: boolean 41 | sider: Sider 42 | header: Header 43 | tab: Tab 44 | /** 主题颜色 */ 45 | primaryColor: string 46 | otherColor: OtherColor 47 | /** 语言 */ 48 | locale: string 49 | /** 名称 */ 50 | name: string 51 | /** Logo */ 52 | logo: string 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from 'vite' 2 | 3 | import { createViteProxy, viteDefine } from './build/config' 4 | import { setupVitePlugins } from './build/plugins' 5 | import { convertEnv, getRootPath, getSrcPath } from './build/utils' 6 | 7 | export default defineConfig(({ mode }) => { 8 | const srcPath = getSrcPath() 9 | const rootPath = getRootPath() 10 | 11 | const viteEnv = convertEnv(loadEnv(mode, process.cwd())) 12 | 13 | const { VITE_PORT, VITE_PUBLIC_PATH, VITE_USE_PROXY, VITE_PROXY_TYPE } = viteEnv 14 | return { 15 | base: VITE_PUBLIC_PATH, 16 | resolve: { 17 | alias: { 18 | '~': rootPath, 19 | '@': srcPath 20 | } 21 | }, 22 | define: viteDefine, 23 | plugins: setupVitePlugins(viteEnv), 24 | server: { 25 | host: '0.0.0.0', 26 | port: VITE_PORT, 27 | open: false, 28 | proxy: createViteProxy(VITE_USE_PROXY, VITE_PROXY_TYPE as ProxyType) 29 | }, 30 | build: { 31 | reportCompressedSize: false, 32 | sourcemap: false, 33 | chunkSizeWarningLimit: 1024, // chunk 大小警告的限制(单位kb) 34 | commonjsOptions: { 35 | ignoreTryCatch: false 36 | } 37 | } 38 | } 39 | }) 40 | --------------------------------------------------------------------------------