├── .devcontainer ├── Dockerfile.dev ├── devcontainer.json ├── docker-compose.yml └── init-cmd.sh ├── .dockerignore ├── .env.example ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_template.yml │ ├── feature_request.yml │ └── general_support_request.yml ├── dependabot.yml ├── labeler.yml ├── release-drafter.yml └── workflows │ ├── ci-release-drafter.yml │ ├── ci-tag.yml │ ├── docs-build-runner.yml │ ├── docs-build.yml │ ├── install-ztnet.yml │ ├── labeler.yml │ ├── main_build.yml │ ├── pull_request.yml │ └── stale-issues.yml ├── .gitignore ├── .vscode ├── extensions.json ├── settings.json └── tasks.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── STYLE_GUIDE.md ├── biome.json ├── components.json ├── docker-compose.yml ├── docs ├── .gitignore ├── README.md ├── babel.config.js ├── docs │ ├── Authentication │ │ ├── _category_.json │ │ └── oauth.md │ ├── Basics │ │ ├── _category_.json │ │ ├── features.md │ │ └── info.md │ ├── Contribute │ │ ├── _category_.json │ │ └── info.md │ ├── Installation │ │ ├── FreeBSD.md │ │ ├── _category_.json │ │ ├── docker-compose.md │ │ ├── linux.md │ │ └── options.md │ ├── Licensing Notice │ │ ├── _category_.json │ │ ├── mkworld.md │ │ └── ztnet.md │ ├── Rest Api │ │ ├── .gitignore │ │ ├── Application │ │ │ └── _source │ │ │ │ └── stats.yml │ │ ├── Organization │ │ │ ├── Organization │ │ │ │ └── _category_.json │ │ │ ├── Users │ │ │ │ └── _category_.json │ │ │ ├── _category_.json │ │ │ └── _source │ │ │ │ ├── network.yml │ │ │ │ ├── networkMember.yml │ │ │ │ ├── organization.yml │ │ │ │ └── users.yml │ │ ├── Personal │ │ │ ├── _category_.json │ │ │ └── _source │ │ │ │ ├── network.yml │ │ │ │ ├── networkMember.yml │ │ │ │ └── user.yml │ │ ├── _category_.json │ │ ├── _example │ │ │ ├── NetworkExample.yml │ │ │ ├── NetworkMemberExample.yml │ │ │ ├── OrganizationExample.yml │ │ │ ├── StatsExample.yml │ │ │ └── UserExample.yml │ │ ├── _http_responses │ │ │ ├── InternalServerError.yml │ │ │ ├── RateLimitExceeded.yml │ │ │ └── Unauthorized.yml │ │ └── _schema │ │ │ ├── NetworkMemberSchema.yml │ │ │ ├── NetworkSchema.yml │ │ │ ├── OrganizationSchema.yml │ │ │ ├── StatsSchema.yml │ │ │ ├── UserSchema.yml │ │ │ └── security.yml │ ├── Showcase │ │ ├── _category_.json │ │ └── images.md │ ├── Usage │ │ ├── _category_.json │ │ ├── create_dns_host.md │ │ ├── migrate.md │ │ ├── override_default_route.md │ │ ├── private_root.md │ │ └── webhooks.md │ ├── _tutorial-basics │ │ ├── _category_.json │ │ ├── congratulations.md │ │ ├── create-a-blog-post.md │ │ ├── create-a-document.md │ │ ├── create-a-page.md │ │ ├── deploy-your-site.md │ │ └── markdown-features.mdx │ └── _usage │ │ ├── _category_.json │ │ └── import_controller.md ├── docusaurus.config.js ├── ecosystem.config.js ├── images │ ├── logo │ │ ├── old │ │ │ ├── ztnet_100x100.png │ │ │ ├── ztnet_16x16.png │ │ │ ├── ztnet_200x200.png │ │ │ ├── ztnet_original.png │ │ │ └── ztnet_social.png │ │ ├── ztnet_100x89.png │ │ ├── ztnet_16x14.png │ │ ├── ztnet_200x178.png │ │ ├── ztnet_300x267.png │ │ ├── ztnet_300x300.png │ │ ├── ztnet_512x512.png │ │ ├── ztnet_orginal.png │ │ ├── ztnet_social.jpg │ │ └── ztnet_test.png │ ├── showcase │ │ ├── admin_controller.jpg │ │ ├── admin_mail.jpg │ │ ├── admin_users.jpg │ │ ├── member_options.jpg │ │ ├── members_table.jpg │ │ ├── network_local.jpg │ │ ├── network_page.jpg │ │ ├── organization_layout.jpg │ │ └── profile.jpg │ ├── webhooks │ │ ├── zapier_actions.jpg │ │ └── zapier_hook.jpg │ ├── ztnet_logo.psd │ └── ztnet_social.psd ├── package-lock.json ├── package.json ├── sidebars.js ├── src │ ├── components │ │ └── HomepageFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.module.css │ │ └── index.tsx ├── static │ ├── .nojekyll │ ├── google46bf74768b883ad6.html │ ├── img │ │ ├── admin │ │ │ └── controller │ │ │ │ ├── unlinked_networks.png │ │ │ │ └── zerotier_api_url.png │ │ ├── favicon.ico │ │ ├── member_options.jpg │ │ ├── network_page.jpg │ │ ├── undraw_docusaurus_mountain.svg │ │ ├── undraw_docusaurus_react.svg │ │ ├── undraw_docusaurus_tree.svg │ │ ├── usage │ │ │ └── vpn_passthrough.png │ │ ├── ztnet_100x89.png │ │ ├── ztnet_landing.jpg │ │ ├── ztnet_landing.png │ │ └── ztnet_social.jpg │ └── robots.txt └── tsconfig.json ├── init-db.sh ├── install.ztnet ├── .gitignore ├── README.md ├── bash │ ├── error.sh │ └── ztnet.sh ├── ecosystem.config.js ├── nginx │ └── install.ztnet.conf ├── package-lock.json ├── package.json ├── src │ ├── index.ts │ ├── project-config.ts │ ├── routes │ │ ├── getBashInstaller.ts │ │ ├── getBinary.ts │ │ ├── health.ts │ │ └── postError.ts │ └── utils │ │ └── helpers.ts └── tsconfig.json ├── jest.api.config.ts ├── jest.pages.config.ts ├── jest.setup.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── prisma ├── migrations │ ├── 20230402192938_ │ │ └── migration.sql │ ├── 20230714081023_flow │ │ └── migration.sql │ ├── 20230717095341_tags │ │ └── migration.sql │ ├── 20230717150852_nw_member │ │ └── migration.sql │ ├── 20230723084736_mailoptions │ │ └── migration.sql │ ├── 20230724093114_nw_description │ │ └── migration.sql │ ├── 20230728082722_init │ │ └── migration.sql │ ├── 20230806115334_zt_central │ │ └── migration.sql │ ├── 20230808053939_db_rework │ │ └── migration.sql │ ├── 20230809202431_cascade │ │ └── migration.sql │ ├── 20230811063619_user_options │ │ └── migration.sql │ ├── 20230813195808_user_group │ │ └── migration.sql │ ├── 20230815175550_custom_root │ │ └── migration.sql │ ├── 20230818113722_custom_root_extend │ │ └── migration.sql │ ├── 20230823185550_update_cascade_delete │ │ └── migration.sql │ ├── 20230825053528_user_invitation │ │ └── migration.sql │ ├── 20230910161252_authorize_warning │ │ └── migration.sql │ ├── 20231006203924_landing_page_body │ │ └── migration.sql │ ├── 20231028155404_token_user_expiration │ │ └── migration.sql │ ├── 20231030072137_userid │ │ └── migration.sql │ ├── 20231203075656_organization │ │ └── migration.sql │ ├── 20231204170707_cascade_delete_user │ │ └── migration.sql │ ├── 20231221190212_member_id_as_name │ │ └── migration.sql │ ├── 20231222102756_oauth │ │ └── migration.sql │ ├── 20231229104733_webhooks │ │ └── migration.sql │ ├── 20240111172646_root_nodes │ │ └── migration.sql │ ├── 20240310090305_physical_address │ │ └── migration.sql │ ├── 20240315064831_api_token_extended │ │ └── migration.sql │ ├── 20240327130439_org_invites │ │ └── migration.sql │ ├── 20240514144732_user_created_at │ │ └── migration.sql │ ├── 20240517203421_routes_and_invitations │ │ └── migration.sql │ ├── 20240803114906_global_node_naming │ │ └── migration.sql │ ├── 20240805143520_totp │ │ └── migration.sql │ ├── 20240809164834_oauth_provider │ │ └── migration.sql │ ├── 20240826093637_user_device │ │ └── migration.sql │ ├── 20240827062445_device_notification │ │ └── migration.sql │ ├── 20241215102802_sitename │ │ └── migration.sql │ ├── 20241227084426_routes_table │ │ └── migration.sql │ ├── 20250415055909_member_authorized │ │ └── migration.sql │ └── migration_lock.toml ├── schema.prisma ├── seed.ts └── seeds │ ├── update-user-id.ts │ └── user-option.seed.ts ├── public ├── favicon.ico ├── images │ └── ztnet_200x178.png ├── manifest.json ├── service-worker.js └── ztnet_300x300.png ├── src ├── __tests__ │ ├── __mocks__ │ │ └── networkById.ts │ ├── components │ │ ├── inputField.test.tsx │ │ ├── loginForm.test.tsx │ │ ├── modal.test.tsx │ │ ├── networkDns.test.tsx │ │ ├── networkIpAssignments.test.tsx │ │ └── networkMulticast.test.tsx │ └── pages │ │ ├── auth │ │ └── signin.test.tsx │ │ └── network │ │ ├── [id].test.tsx │ │ └── index.test.tsx ├── components │ ├── adminPage │ │ ├── controller │ │ │ ├── createPlanet.tsx │ │ │ ├── debugController.tsx │ │ │ ├── privateRoot.tsx │ │ │ ├── rootForm.tsx │ │ │ ├── unlinkedNetworkTable.tsx │ │ │ └── zerotierUrl.tsx │ │ ├── mail │ │ │ ├── mailDeviceIpChangeNotificationTemplate.tsx │ │ │ ├── mailForgotPasswordTemplate.tsx │ │ │ ├── mailNewDeviceNotificationTemplate.tsx │ │ │ ├── mailNotificationTemplate.tsx │ │ │ ├── mailOrganizationInviteTemplate.tsx │ │ │ ├── mailUserInviteTemplate.tsx │ │ │ └── mailVerifyEmail.tsx │ │ └── users │ │ │ ├── table │ │ │ └── accounts.tsx │ │ │ ├── userGroup.tsx │ │ │ ├── userGroups.tsx │ │ │ ├── userInvitation.tsx │ │ │ ├── userIsActive.tsx │ │ │ ├── userOptionsModal.tsx │ │ │ └── userRole.tsx │ ├── auth │ │ ├── credentialsForm.tsx │ │ ├── forgotPasswordForm.tsx │ │ ├── formInput.tsx │ │ ├── formSubmitButton.tsx │ │ ├── mfaRecoveryForm.tsx │ │ ├── multifactorNotEnabledAlert.tsx │ │ ├── oauthLogin.tsx │ │ ├── registerForm.tsx │ │ ├── registerOrganizationInvite.tsx │ │ ├── totpDigits.tsx │ │ ├── totpDisable.tsx │ │ ├── totpInput.tsx │ │ ├── totpSetup.tsx │ │ ├── userDevices.tsx │ │ ├── welcomeMessage.tsx │ │ └── withAuth.tsx │ ├── elements │ │ ├── debouncedInput.tsx │ │ ├── dropdownlist.tsx │ │ ├── input.tsx │ │ ├── inputField.tsx │ │ ├── multiSelect.tsx │ │ └── textarea.tsx │ ├── layouts │ │ ├── chatAside.tsx │ │ ├── footer.tsx │ │ ├── header.tsx │ │ ├── layout.tsx │ │ ├── logFooter.tsx │ │ └── sidebar.tsx │ ├── networkByIdPage │ │ ├── addMemberById.tsx │ │ ├── anotation.tsx │ │ ├── flowRule │ │ │ ├── flagsAndTags.tsx │ │ │ ├── tagComponent.tsx │ │ │ └── useFlagsAndTags.tsx │ │ ├── inviteMemberbyMail.tsx │ │ ├── ipv4Assignment.tsx │ │ ├── ipv6assignment.tsx │ │ ├── memberOptionsModal.tsx │ │ ├── networkDescription.tsx │ │ ├── networkDns.tsx │ │ ├── networkFlowRules.tsx │ │ ├── networkHelp.tsx │ │ ├── networkIpAssignments.tsx │ │ ├── networkMtu.tsx │ │ ├── networkMulticast.tsx │ │ ├── networkName.tsx │ │ ├── networkPrivatePublic.tsx │ │ ├── networkQrCode.tsx │ │ ├── networkRoutes │ │ │ ├── collumns.tsx │ │ │ ├── networkRoutes.tsx │ │ │ ├── networkRoutesTable.tsx │ │ │ └── routesEditCell.tsx │ │ ├── organization │ │ │ ├── addOrgForm.tsx │ │ │ └── listOrganizations.tsx │ │ ├── privatePublic.tsx │ │ ├── table │ │ │ ├── deletedNetworkMembersTable.tsx │ │ │ ├── memberEditCell.tsx │ │ │ ├── memberHeaderColumns.tsx │ │ │ └── networkMembersTable.tsx │ │ └── ztCentral │ │ │ └── centralFlowRules.tsx │ ├── networkPage │ │ ├── centralNetworkTable.tsx │ │ ├── networkOptionsModal.tsx │ │ ├── networkTable.tsx │ │ ├── networkTableMemberCount.tsx │ │ └── transferNetworkToOrganization.tsx │ ├── organization │ │ ├── deleteOrganizationModal.tsx │ │ ├── editUserModal.tsx │ │ ├── inviteByMail.tsx │ │ ├── inviteByUser.tsx │ │ ├── networkTable.tsx │ │ ├── orgNavBar.tsx │ │ ├── orgUserRole.tsx │ │ └── userTable.tsx │ ├── shared │ │ ├── menuSectionDividerWrapper.tsx │ │ ├── metaTags.tsx │ │ ├── modal.tsx │ │ ├── networkLoadingSkeleton.tsx │ │ ├── tableFilter.tsx │ │ └── tableFooter.tsx │ └── userSettings │ │ ├── apiToken.tsx │ │ └── fontSize.tsx ├── cronTasks.ts ├── env.mjs ├── global.d.ts ├── hooks │ ├── useDynamicViewportHeight.tsx │ ├── useHandleResize.tsx │ ├── useOrganizationWebsocket.tsx │ ├── useSkipper.tsx │ └── useTrpcApiHandler.tsx ├── icons │ ├── IOSIcon.tsx │ ├── androidIcon.tsx │ ├── copy.tsx │ ├── edit.tsx │ ├── monitor.tsx │ ├── plusIcon.tsx │ ├── smartphone.tsx │ ├── tablet.tsx │ ├── verified.tsx │ └── windowsIcon.tsx ├── instrumentation.ts ├── locales │ ├── en │ │ └── common.json │ ├── es │ │ └── common.json │ ├── fr │ │ └── common.json │ ├── lang.ts │ ├── no │ │ └── common.json │ ├── pl │ │ └── common.json │ ├── ru │ │ └── common.json │ ├── zh-tw │ │ └── common.json │ └── zh │ │ └── common.json ├── pages │ ├── 404.tsx │ ├── _app.tsx │ ├── _document.tsx │ ├── admin │ │ ├── backuprestore │ │ │ └── index.tsx │ │ ├── controller │ │ │ └── index.tsx │ │ ├── index.tsx │ │ ├── mail │ │ │ └── index.tsx │ │ ├── notification │ │ │ └── index.tsx │ │ ├── organization │ │ │ └── index.tsx │ │ ├── settings │ │ │ └── index.tsx │ │ └── users │ │ │ └── index.tsx │ ├── api │ │ ├── __tests__ │ │ │ ├── auth │ │ │ │ └── two-factor │ │ │ │ │ ├── enable.test.tsx │ │ │ │ │ └── setup.test.tsx │ │ │ └── v1 │ │ │ │ ├── apiAuthentication.ts │ │ │ │ ├── application │ │ │ │ └── statistic.test.ts │ │ │ │ ├── network │ │ │ │ ├── network.test.ts │ │ │ │ └── networkById.test.ts │ │ │ │ ├── networkMembers │ │ │ │ └── updateMember.test.ts │ │ │ │ ├── org │ │ │ │ ├── network │ │ │ │ │ ├── member │ │ │ │ │ │ ├── org.member.id.test.ts │ │ │ │ │ │ └── org.member.test.ts │ │ │ │ │ ├── org.network.id.test.ts │ │ │ │ │ └── org.network.test.ts │ │ │ │ ├── org.test.ts │ │ │ │ └── orgid.test.ts │ │ │ │ └── user │ │ │ │ └── user.test.ts │ │ ├── auth │ │ │ ├── [...nextauth].ts │ │ │ ├── two-factor │ │ │ │ └── totp │ │ │ │ │ ├── disable.ts │ │ │ │ │ ├── enable.ts │ │ │ │ │ └── setup.ts │ │ │ └── user │ │ │ │ ├── invalidateUser.ts │ │ │ │ └── invalidateUserDevice.ts │ │ ├── mkworld │ │ │ └── config.ts │ │ ├── planet.ts │ │ ├── trpc │ │ │ └── [trpc].ts │ │ ├── v1 │ │ │ ├── network │ │ │ │ ├── [id] │ │ │ │ │ ├── index.ts │ │ │ │ │ └── member │ │ │ │ │ │ ├── [memberId] │ │ │ │ │ │ ├── _schema.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ └── index.ts │ │ │ │ ├── _schema.ts │ │ │ │ └── index.ts │ │ │ ├── org │ │ │ │ ├── [orgid] │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── network │ │ │ │ │ │ ├── [nwid] │ │ │ │ │ │ │ ├── _schema.ts │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── member │ │ │ │ │ │ │ │ ├── [memberId] │ │ │ │ │ │ │ │ ├── _schema.ts │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── _schema.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── user │ │ │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ │ ├── stats │ │ │ │ └── index.ts │ │ │ └── user │ │ │ │ ├── _schema.ts │ │ │ │ └── index.ts │ │ └── websocket │ │ │ └── index.ts │ ├── auth │ │ ├── forgotPassword │ │ │ ├── index.tsx │ │ │ └── reset │ │ │ │ └── index.tsx │ │ ├── login │ │ │ └── index.tsx │ │ ├── mfaRecovery │ │ │ ├── index.tsx │ │ │ └── reset │ │ │ │ └── index.tsx │ │ ├── register │ │ │ └── index.tsx │ │ └── verifyEmail │ │ │ └── index.tsx │ ├── central │ │ ├── [id].tsx │ │ └── index.tsx │ ├── dashboard │ │ └── index.tsx │ ├── network │ │ ├── [id].tsx │ │ └── index.tsx │ ├── organization │ │ ├── [orgid].tsx │ │ └── [orgid] │ │ │ ├── [id].tsx │ │ │ └── admin │ │ │ ├── index.tsx │ │ │ ├── invite │ │ │ └── index.tsx │ │ │ ├── network │ │ │ └── index.tsx │ │ │ ├── settings │ │ │ └── index.tsx │ │ │ └── webhooks │ │ │ └── index.tsx │ └── user-settings │ │ ├── account │ │ └── index.tsx │ │ ├── index.tsx │ │ ├── network │ │ └── index.tsx │ │ └── notification │ │ └── index.tsx ├── server │ ├── api │ │ ├── __tests__ │ │ │ ├── network │ │ │ │ ├── getNetworkById.test.ts │ │ │ │ └── getUserNetworks.test.ts │ │ │ └── networkMembers │ │ │ │ └── updateDatabaseOnly.test.ts │ │ ├── root.ts │ │ ├── routers │ │ │ ├── _schema.ts │ │ │ ├── adminRoute.ts │ │ │ ├── authRouter.ts │ │ │ ├── memberRouter.ts │ │ │ ├── mfaAuthRouter.ts │ │ │ ├── networkRouter.ts │ │ │ ├── organizationRouter.ts │ │ │ ├── publicRouter.ts │ │ │ └── settingsRouter.ts │ │ ├── services │ │ │ ├── authService.ts │ │ │ ├── memberService.ts │ │ │ ├── metricService.ts │ │ │ ├── networkService.ts │ │ │ ├── organizationAuthService.ts │ │ │ └── routesService.ts │ │ ├── trpc.ts │ │ └── utils │ │ │ ├── ipUtils.ts │ │ │ └── memberUtils.ts │ ├── auth.ts │ ├── callbacks │ │ ├── jwt.ts │ │ ├── session.ts │ │ └── signin.ts │ ├── db.ts │ ├── getServerSideProps.ts │ └── helpers │ │ └── errorHandler.ts ├── styles │ └── globals.css ├── types │ ├── apiTypes.ts │ ├── backupRestore.ts │ ├── central │ │ ├── controllerStatus.d.ts │ │ ├── members.d.ts │ │ └── network.d.ts │ ├── ctx.ts │ ├── errorHandling.d.ts │ ├── invitation.ts │ ├── local │ │ ├── member.d.ts │ │ └── network.d.ts │ ├── network.d.ts │ ├── webhooks.ts │ ├── worldConfig.d.ts │ └── ztController.ts └── utils │ ├── IPv4gen.ts │ ├── IPv6.ts │ ├── api.ts │ ├── apiRouteAuth.ts │ ├── devices.ts │ ├── docker.ts │ ├── downloadFile.ts │ ├── encryption.ts │ ├── enums.ts │ ├── errorCode.ts │ ├── errors.tsx │ ├── fakeData.ts │ ├── global.ts │ ├── isIpInsubnet.ts │ ├── localstorage.ts │ ├── mail.ts │ ├── planet.ts │ ├── randomColor.ts │ ├── rateLimit.ts │ ├── role.ts │ ├── rule-compiler.js │ ├── sorting.ts │ ├── store.ts │ ├── time.ts │ ├── webhook.ts │ └── ztApi.ts ├── tailwind.config.cjs ├── tsconfig.json └── ztnodeid ├── Dockerfile ├── assets └── mkworld.config.json ├── build ├── freebsd_amd64 │ └── ztmkworld ├── linux_amd64 │ └── ztmkworld └── linux_arm64 │ └── ztmkworld ├── cmd └── mkworld │ └── main.go ├── go.mod ├── go.sum └── pkg ├── node ├── errs.go ├── identity.go ├── node.go └── world.go └── ztcrypto └── identity.go /.devcontainer/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:22.1-bookworm-slim 2 | 3 | ENV NODE_ENV development 4 | RUN apt update && apt install -y git curl sudo postgresql-client procps nano 5 | 6 | # RUN chmod +x .devcontainer/init-cmd.sh 7 | 8 | # USER nextjs 9 | 10 | # expose port 3000 for application and 5555 for prisma studio 11 | EXPOSE 3000 5555 12 | 13 | ENV PORT 3000 14 | ENTRYPOINT ["/workspaces/.devcontainer/init-cmd.sh"] 15 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | # db 3 | postgres: 4 | ports: 5 | - 5432:5432 6 | 7 | # zt container 8 | zerotier: 9 | image: zyclonite/zerotier:1.14.2 10 | ports: 11 | - "9993:9993" 12 | environment: 13 | - ZT_OVERRIDE_LOCAL_CONF=true 14 | - ZT_ALLOW_MANAGEMENT_FROM=0.0.0.0/0 15 | # ztnet 16 | ztnet: 17 | build: 18 | context: . 19 | dockerfile: .devcontainer/Dockerfile.dev 20 | args: 21 | NEXT_PUBLIC_CLIENTVAR: "clientvar" 22 | 23 | working_dir: /workspaces 24 | volumes: 25 | - .:/workspaces:cached 26 | environment: 27 | NEXTAUTH_URL: "${NEXTAUTH_URL:-http://10.0.0.217:3000}" 28 | networks: 29 | - app-network 30 | depends_on: 31 | - postgres 32 | - zerotier 33 | -------------------------------------------------------------------------------- /.devcontainer/init-cmd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Create .env file 6 | # cat << EOF > .env 7 | # DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}?schema=public 8 | # ZT_ADDR=${ZT_ADDR} 9 | # EOF 10 | 11 | until PGPASSWORD=$POSTGRES_PASSWORD psql -h "$POSTGRES_HOST" -U "$POSTGRES_USER" -c '\q'; do 12 | >&2 echo "Postgres is unavailable - sleeping" 13 | sleep 1 14 | done 15 | 16 | # Check architecture and copy the corresponding file if it exists 17 | ARCH=$(uname -m) 18 | if [ "$ARCH" = "x86_64" ] && [ -f "/workspaces/ztnodeid/build/linux_amd64/ztmkworld" ]; then 19 | cp /workspaces/ztnodeid/build/linux_amd64/ztmkworld /usr/local/bin/ztmkworld 20 | elif [ "$ARCH" = "aarch64" ] && [ -f "/workspaces/ztnodeid/build/linux_arm64/ztmkworld" ]; then 21 | cp /workspaces/ztnodeid/build/linux_arm64/ztmkworld /usr/local/bin/ztmkworld 22 | fi 23 | chmod +x /usr/local/bin/ztmkworld 24 | 25 | # apply migrations to the database 26 | echo "Applying migrations to the database..." 27 | npx prisma migrate deploy 28 | echo "Migrations applied successfully!" 29 | 30 | while sleep 1000; do :; done -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .env 2 | Dockerfile 3 | .dockerignore 4 | node_modules 5 | npm-debug.log 6 | README.md 7 | .next 8 | .git -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Since the ".env" file is gitignored, you can use the ".env.example" file to 2 | # build a new ".env" file when you clone the repo. Keep this file up-to-date 3 | # when you add new variables to `.env`. 4 | 5 | # This file will be committed to version control, so make sure not to have any 6 | # secrets in it. If you are cloning this repo, create a copy of this file named 7 | # ".env" and populate it with your secrets. 8 | 9 | # When adding additional environment variables, the schema in "/src/env.mjs" 10 | # should be updated accordingly. 11 | 12 | # Next Auth 13 | NEXTAUTH_URL="http://localhost:3000" 14 | 15 | # You can generate a new secret on the command line with: 16 | # openssl rand -base64 32 17 | # https://next-auth.js.org/configuration/options#secret 18 | # NEXTAUTH_SECRET="" 19 | NEXTAUTH_SECRET="change_me" 20 | 21 | # Prisma 22 | # https://www.prisma.io/docs/reference/database-reference/connection-urls#env 23 | POSTGRES_HOST="postgres" # use "localhost" if developing in tradional way. postgres is the hostname of the container 24 | POSTGRES_USER="postgres" 25 | POSTGRES_PASSWORD="postgres" 26 | POSTGRES_PORT=5432 27 | POSTGRES_DB="ztnet" 28 | DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${POSTGRES_PORT}/${POSTGRES_DB}?schema=public" 29 | 30 | # prisma migrate uses a different env variable for the shaddow database url 31 | # https://www.prisma.io/docs/concepts/components/prisma-migrate/shadow-database 32 | MIGRATE_POSTGRES_DB="shaddow_ztnet" 33 | MIGRATE_DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${POSTGRES_PORT}/${MIGRATE_POSTGRES_DB}?schema=public" 34 | 35 | # OAuth 36 | OAUTH_ALLOW_DANGEROUS_EMAIL_LINKING=true 37 | OAUTH_WELLKNOWN="https://accounts.google.com/.well-known/openid-configuration" 38 | OAUTH_ID= 39 | OAUTH_SECRET= 40 | OAUTH_EXCLUSIVE_LOGIN=false 41 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.{cmd,[cC][mM][dD]} text eol=crlf 3 | *.{bat,[bB][aA][tT]} text eol=crlf -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: sinamics # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for ztnet 3 | title: "[Feature Request]: " 4 | labels: ["enhancement"] 5 | assignees: [] 6 | 7 | body: 8 | - type: markdown 9 | id: instructions 10 | attributes: 11 | value: "## Thank you for your interest in improving ztnet! Please provide as much information as possible in the sections below.\n" 12 | 13 | - type: input 14 | id: feature_short_description 15 | attributes: 16 | label: "🚀 Feature Summary" 17 | description: "Provide a brief, concise summary of the feature." 18 | validations: 19 | required: true 20 | 21 | - type: textarea 22 | id: feature_detailed_description 23 | attributes: 24 | label: "📝 Detailed Description" 25 | description: "Describe the proposed feature in detail. Include specifics about what it should do, how it should work, and what the expected benefits are." 26 | validations: 27 | required: false 28 | 29 | - type: textarea 30 | id: use_case 31 | attributes: 32 | label: "🎯 Use Case" 33 | description: "Describe the specific use case(s) or problem(s) this feature would address." 34 | validations: 35 | required: false 36 | 37 | - type: dropdown 38 | id: willing_to_contribute 39 | attributes: 40 | label: "💡 Willing to Contribute" 41 | description: "Would you be willing to contribute to the implementation or testing of this feature?" 42 | options: 43 | - "Yes, I could help with coding" 44 | - "Yes, I could help with testing" 45 | - "No, I can only suggest the feature but cannot help in development or testing" 46 | validations: 47 | required: false 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/general_support_request.yml: -------------------------------------------------------------------------------- 1 | name: Support Request 2 | description: Submit a request for assistance or inquiry about ZTNET 3 | title: "[Support]: " 4 | labels: ["support"] 5 | 6 | body: 7 | - type: textarea 8 | id: description 9 | attributes: 10 | label: "📝 Inquiry" 11 | description: "Please provide a detailed description of your inquiry, issue, or the assistance you require with ZTNET." 12 | validations: 13 | required: true 14 | - type: input 15 | id: version 16 | attributes: 17 | label: "🔖 Version" 18 | description: "Please provide the ztnet version tag" 19 | validations: 20 | required: true 21 | - type: checkboxes 22 | id: deployment-type 23 | attributes: 24 | label: "🔧 Deployment Type" 25 | description: "Select the type of ZTNET deployment you are using." 26 | options: 27 | - label: Docker 28 | - label: Standalone 29 | validations: 30 | required: true 31 | - type: dropdown 32 | id: os 33 | attributes: 34 | label: "💻 Operating System" 35 | options: 36 | - Debian 37 | - Ubuntu 38 | - Other Linux 39 | - Windows 40 | - Other 41 | validations: 42 | required: false 43 | - type: dropdown 44 | id: browser 45 | attributes: 46 | label: "🌐 Browser" 47 | description: "Which browser are you using?" 48 | options: 49 | - Chrome 50 | - Firefox 51 | - Brave 52 | - Safari 53 | - Edge 54 | - Opera 55 | - Other 56 | validations: 57 | required: true 58 | - type: textarea 59 | id: other 60 | attributes: 61 | label: "📚 Any Other Information That May Be Helpful" 62 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | # Add 'ztnet' label to any file changes within 'root' EXCEPT for the docs and install sub-folder 2 | ztnet: 3 | - all: 4 | - changed-files: 5 | - any-glob-to-any-file: '**' 6 | - all-globs-to-all-files: ['!docs/*', '!install.ztnet/*'] 7 | 8 | # Add 'Documentation' label to any file changes within 'docs' folders 9 | documentation: 10 | - changed-files: 11 | - any-glob-to-any-file: 12 | - docs/** 13 | 14 | # Add 'installer' label to any file changes within 'install.ztnet' folders 15 | installer: 16 | - changed-files: 17 | - any-glob-to-any-file: 18 | - install.ztnet/** -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | categories: 4 | - title: '🛡️ Ztnet' 5 | labels: 6 | - 'ztnet' 7 | - title: '📝 Documentation' 8 | labels: 9 | - 'documentation' 10 | - title: '📦 Installer' 11 | labels: 12 | - 'installer' 13 | - title: '🚀 Features' 14 | labels: 15 | - 'feature' 16 | - 'enhancement' 17 | - title: '🐛 Bug Fixes' 18 | labels: 19 | - 'fix' 20 | - 'bugfix' 21 | - 'bug' 22 | change-template: '- $TITLE (#$NUMBER)' 23 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 24 | exclude-contributors: 25 | - sinamics 26 | version-resolver: 27 | major: 28 | labels: 29 | - 'major' 30 | minor: 31 | labels: 32 | - 'feature' 33 | patch: 34 | labels: 35 | - 'bugfix' 36 | default: patch 37 | 38 | template: | 39 | ## Release Notes 40 | 41 | $CHANGES 42 | 43 | **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...$RESOLVED_VERSION 44 | 45 | ### Documentation 46 | https://ztnet.network 47 | 48 | Contributors to this release: $CONTRIBUTORS 🎉 49 | 50 | sort-direction: 'ascending' 51 | -------------------------------------------------------------------------------- /.github/workflows/ci-release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | pull_request: 5 | types: [closed] 6 | 7 | workflow_dispatch: 8 | permissions: 9 | # write permission is required to create a github release 10 | contents: write 11 | pull-requests: write 12 | jobs: 13 | update_release_draft: 14 | permissions: 15 | contents: write 16 | pull-requests: write 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2 20 | with: 21 | fetch-depth: 0 # Required due to the weg Git works, without it this action won't be able to find any or the correct tags 22 | ref: main 23 | 24 | - name: version 25 | id: version 26 | run: | 27 | tag=${GITHUB_REF/refs\/tags\//} 28 | version=${tag#v} 29 | major=${version%%.*} 30 | echo "tag=${tag}" >> $GITHUB_OUTPUT 31 | echo "version=${version}" >> $GITHUB_OUTPUT 32 | echo "major=${major}" >> $GITHUB_OUTPUT 33 | 34 | - name: Automatic release 35 | uses: release-drafter/release-drafter@09c613e259eb8d4e7c81c2cb00618eb5fc4575a7 # v5.25.0 36 | with: 37 | version: ${{ steps.version.outputs.version }} 38 | # tag-template: 'v$RESOLVED_VERSION' 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | -------------------------------------------------------------------------------- /.github/workflows/docs-build-runner.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Docusaurus on runner agent 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "docs/**" 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: [self-hosted, ztnet.network] 14 | defaults: 15 | run: 16 | working-directory: docs 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 18 22 | cache: npm 23 | - name: Install dependencies 24 | run: npm ci 25 | - name: Install Tailwind CSS 26 | run: npm install tailwindcss@3 27 | - name: Build API docs 28 | run: npx docusaurus gen-api-docs all 29 | - name: Build website 30 | run: npm run build 31 | 32 | deploy: 33 | runs-on: [self-hosted, ztnet.network] 34 | defaults: 35 | run: 36 | working-directory: docs 37 | needs: build 38 | steps: 39 | - name: Move Website to Temporary Directory 40 | run: | 41 | # Define a directory within the user's home for the build 42 | BUILD_DIR=~/ztnet_docs 43 | 44 | # Remove any existing build directory 45 | rm -rf $BUILD_DIR 46 | 47 | # Create a new build directory 48 | mkdir -p $BUILD_DIR 49 | 50 | # Move the new build to the directory 51 | mv build $BUILD_DIR 52 | 53 | - name: Restart Server 54 | run: pm2 restart ecosystem.config.js -------------------------------------------------------------------------------- /.github/workflows/docs-build.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Docusaurus GH-Pages 2 | 3 | on: 4 | # push: 5 | # branches: 6 | # - main 7 | # paths: 8 | # - "docs/**" 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | defaults: 15 | run: 16 | working-directory: docs 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 18 22 | cache: npm 23 | - name: Install dependencies 24 | run: npm install 25 | - name: Install Tailwind CSS 26 | run: npm install tailwindcss 27 | - name: Build API docs 28 | run: npx docusaurus gen-api-docs all 29 | - name: Build website 30 | run: npm run build 31 | - name: Upload artifact 32 | uses: actions/upload-pages-artifact@v2 33 | with: 34 | # Upload entire repository 35 | path: docs/build 36 | 37 | deploy: 38 | environment: 39 | name: github-pages 40 | url: ${{ steps.deployment.outputs.page_url }} 41 | runs-on: ubuntu-latest 42 | needs: build 43 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 44 | permissions: 45 | pages: write # to deploy to Pages 46 | id-token: write # to verify the deployment originates from an appropriate source 47 | steps: 48 | - name: Deploy to GitHub Pages 🚀 49 | id: deployment 50 | uses: actions/deploy-pages@v2 51 | -------------------------------------------------------------------------------- /.github/workflows/install-ztnet.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to install.ztnet.network 2 | 3 | # Controls when the workflow will run 4 | on: 5 | push: 6 | branches: 7 | - main 8 | paths: 9 | - 'install.ztnet/**' 10 | workflow_dispatch: 11 | 12 | env: 13 | LAST_UPDATED: ${{ github.event.head_commit.timestamp }} 14 | 15 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 16 | jobs: 17 | # This workflow contains a single job called "build" 18 | prepear: 19 | # The type of runner that the job will run on 20 | runs-on: [self-hosted, ztnet.installer] 21 | defaults: 22 | run: 23 | working-directory: install.ztnet 24 | steps: 25 | - uses: actions/checkout@v2 26 | with: 27 | clean: false 28 | - name: Enviorment 29 | run: | 30 | touch .env 31 | echo NODE_ENV="production" > .env 32 | echo NODE_MAILER_USER="${{ secrets.NODE_MAILER_USER }}" >> .env 33 | echo NODE_MAILER_PASSWORD="${{ secrets.NODE_MAILER_PASSWORD }}" >> .env 34 | 35 | - name: install dependencies 36 | run: npm install 37 | 38 | deploy: 39 | runs-on: [self-hosted, ztnet.installer] 40 | defaults: 41 | run: 42 | working-directory: install.ztnet 43 | needs: prepear 44 | steps: 45 | - name: Update Installer Script 46 | run: | 47 | sed -i "s/^INSTALLER_LAST_UPDATED=.*$/INSTALLER_LAST_UPDATED=${{ env.LAST_UPDATED }}/" bash/ztnet.sh 48 | - name: Build Application 49 | run: npm run build 50 | 51 | - name: Restart Server 52 | run: pm2 restart ecosystem.config.js 53 | 54 | - name: Sleep 10sec 55 | run: sleep 10s 56 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Request Labeler" 2 | on: 3 | - pull_request_target 4 | 5 | jobs: 6 | labeler: 7 | permissions: 8 | contents: read 9 | pull-requests: write 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/labeler@v5 -------------------------------------------------------------------------------- /.github/workflows/stale-issues.yml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues 2 | on: 3 | schedule: 4 | - cron: "30 1 * * *" 5 | workflow_dispatch: 6 | jobs: 7 | close-issues: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | steps: 13 | - uses: actions/stale@v5 14 | with: 15 | days-before-issue-stale: 30 16 | days-before-issue-close: 3 17 | stale-issue-label: "stale" 18 | stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." 19 | close-issue-message: "This issue was closed because it has been inactive for 3 days since being marked as stale." 20 | days-before-pr-stale: -1 21 | days-before-pr-close: -1 22 | exempt-issue-labels: "pinned,security" 23 | operations-per-run: 120 24 | repo-token: ${{ secrets.GITHUB_TOKEN }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | /tmp 8 | # testing 9 | /coverage 10 | 11 | # database 12 | /prisma/db.sqlite 13 | /prisma/db.sqlite-journal 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | next-env.d.ts 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # local env files 34 | # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables 35 | .env 36 | .env*.local 37 | 38 | # vercel 39 | .vercel 40 | 41 | # typescript 42 | *.tsbuildinfo 43 | zeronsd.sh* 44 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "prisma.prisma", 4 | "biomejs.biome", 5 | "bradlc.vscode-tailwindcss" 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[javascript]": { 3 | "editor.defaultFormatter": "biomejs.biome" 4 | }, 5 | "[typescript]": { 6 | "editor.defaultFormatter": "biomejs.biome" 7 | }, 8 | "editor.defaultFormatter": "biomejs.biome", 9 | "editor.tabSize": 2, 10 | "[typescriptreact]": { 11 | "editor.defaultFormatter": "biomejs.biome" 12 | }, 13 | "editor.codeActionsOnSave": { 14 | "quickfix.biome": "explicit", 15 | "source.organizeImports.biome": "explicit" 16 | }, 17 | "[prisma]": { 18 | "editor.defaultFormatter": "Prisma.prisma" 19 | } 20 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "dev", 7 | "problemMatcher": [], 8 | "label": "Start Development Server", 9 | "detail": "npm run dev" 10 | }, 11 | { 12 | "type": "npm", 13 | "script": "build", 14 | "group": "build", 15 | "problemMatcher": [], 16 | "label": "Build Production Package", 17 | "detail": "npm run build" 18 | }, 19 | { 20 | "type": "npm", 21 | "script": "install", 22 | "group": "none", 23 | "problemMatcher": [], 24 | "label": "Install Dependencies from Package", 25 | "detail": "npm install" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /STYLE_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Style Guide 2 | 3 | ## Naming Conventions 4 | 5 | - Use meaningful and descriptive names for variables, functions, and classes. 6 | - Use CamelCase for classes, functions and variables. 7 | - Use uppercase for constants. 8 | 9 | ## Typescript 10 | 11 | - Use `const` for variables that are not reassigned and `let` for variables that are reassigned. 12 | 13 | ```typescript 14 | const myVar = "foo"; 15 | let myOtherVar = "bar"; 16 | ``` 17 | 18 | - Use === and !== for equality and inequality comparisons. 19 | 20 | ```typescript 21 | if (myVar === myOtherVar) { 22 | console.log("They are equal!"); 23 | } else { 24 | console.log("They are not equal!"); 25 | } 26 | ``` 27 | 28 | - Use arrow functions for anonymous functions. 29 | 30 | ```typescript 31 | const myFunc = () => { 32 | console.log("Hello World!"); 33 | }; 34 | ``` 35 | 36 | - Use async/await for asynchronous code. 37 | 38 | ```typescript 39 | const myAsyncFunc = async () => { 40 | await someAsyncOperation(); 41 | console.log("Done!"); 42 | }; 43 | ``` 44 | 45 | ## Miscellaneous 46 | 47 | - Keep code DRY (Don't Repeat Yourself). 48 | - Avoid long functions or methods. 49 | - Avoid magic numbers or hard-coded values. 50 | - Write code that is easy to read and understand. 51 | - Follow established conventions and patterns within the codebase. 52 | 53 | ## Conclusion 54 | 55 | By following these guidelines, we can create code that is consistent, maintainable, and easy to read. If you have any questions or suggestions for improving the style guide, please reach out to the project maintainers. 56 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.4.0/schema.json", 3 | "organizeImports": { 4 | "enabled": false 5 | }, 6 | "files": { 7 | "ignore": [ 8 | "**/*.js", 9 | "install.ztnet/**/*", 10 | ".devcontainer/**/*" 11 | ] 12 | }, 13 | "formatter": { 14 | "enabled": true, 15 | "formatWithErrors": false, 16 | "indentStyle": "tab", 17 | "indentWidth": 2, 18 | "lineWidth": 90, 19 | "ignore": [ 20 | "src/locales/**/*.json" 21 | ] 22 | }, 23 | "linter": { 24 | "enabled": true, 25 | "rules": { 26 | "recommended": true, 27 | "style": { 28 | "useSelfClosingElements": "off", 29 | "noNonNullAssertion": "off", 30 | "useImportType": "off", 31 | "useNumberNamespace": "off", 32 | "useNodejsImportProtocol": "off" 33 | }, 34 | "a11y": { 35 | "noNoninteractiveTabindex": "off", 36 | "useButtonType": "off", 37 | "noSvgWithoutTitle": "off", 38 | "useValidAnchor": "off", 39 | "useKeyWithMouseEvents": "off", 40 | "useKeyWithClickEvents": "off", 41 | "useValidAriaRole": "off", 42 | "noLabelWithoutControl": "off", 43 | "useSemanticElements": "off" 44 | }, 45 | "suspicious": { 46 | "noCommentText": "warn", 47 | "noConsoleLog": "error", 48 | "noExplicitAny": "error" 49 | }, 50 | "correctness": { 51 | "noUnusedVariables": "error" 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "~/components", 15 | "utils": "~/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | npm install 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | npm start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | npm run build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | 28 | # API docs 29 | Api is built on OpenAPI and the source needs to be built before it can be used by the website. 30 | 31 | ### Build 32 | ``` 33 | npx docusaurus gen-api-docs all 34 | ``` 35 | 36 | ### Clean 37 | ``` 38 | npx docusaurus clean-api-docs all 39 | ``` 40 | 41 | ### Combined 42 | ``` 43 | npx docusaurus clean-api-docs all && npx docusaurus gen-api-docs all 44 | ``` -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/docs/Authentication/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Authentication", 3 | "position": 2, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } -------------------------------------------------------------------------------- /docs/docs/Basics/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Basics", 3 | "position": 7, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } -------------------------------------------------------------------------------- /docs/docs/Basics/info.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: info 3 | title: How ZTNET Works 4 | slug: /how-ztnet-works 5 | description: How ZTNET works 6 | sidebar_position: 4 7 | --- 8 | 9 | # How ZTNET Works 10 | 11 | ## Overview 12 | 13 | ZTnet is a web-based user interface developed in TypeScript, designed to facilitate easier management of ZeroTier networks. It works as an intermediary between the end-user and the ZeroTier Controller API, providing an intuitive interface for network management tasks. 14 | 15 | ## Core Components 16 | 17 | ### Frontend 18 | 19 | The frontend is built using TypeScript along with modern web frameworks to offer an interactive and user-friendly experience. 20 | 21 | ### Backend 22 | 23 | The backend acts as the bridge between the frontend and the ZeroTier Controller API, also written in TypeScript. It handles API requests and performs data transformation, making it easier to manage the network configurations. 24 | 25 | ## Communication Flow 26 | 27 | 1. **User Input**: The user performs actions on the ZTnet web UI. 28 | 2. **API Requests**: ZTnet translates these actions into API requests. 29 | 3. **ZeroTier Controller API**: These requests are sent to the ZeroTier Controller API for processing. 30 | 4. **API Responses**: The API sends back the data or status update. 31 | 5. **Display Data**: ZTnet then takes this information and updates the UI accordingly. 32 | 33 | ## ZeroTier API Reference 34 | 35 | For more details about the API endpoints and data formats used, you can refer to the [ZeroTier API Documentation](https://docs.zerotier.com/service/v1/). 36 | 37 | ## Language and Libraries 38 | 39 | - **Language**: TypeScript 40 | - **Key Libraries**: React, Next.js 41 | 42 | ## Conclusion 43 | 44 | Understanding the basic structure and flow of ZTnet can help both end-users and developers get the most out of the application. The modular design and the use of TypeScript make it scalable and easy to contribute to. 45 | -------------------------------------------------------------------------------- /docs/docs/Contribute/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Contribute", 3 | "position": 8, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } -------------------------------------------------------------------------------- /docs/docs/Installation/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Installation", 3 | "position": 1, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } -------------------------------------------------------------------------------- /docs/docs/Licensing Notice/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Licensing Notes", 3 | "position": 9, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } -------------------------------------------------------------------------------- /docs/docs/Licensing Notice/mkworld.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: mkworld 3 | title: Mkworld 4 | slug: /licensing/mkworld 5 | description: Comprehensive licensing information and attribution for Mkworld's Go adaptation used in ZTNET. 6 | sidebar_position: 5 7 | --- 8 | 9 | # Mkworld Tool 10 | 11 | ### 📄 Attribution and Licensing Notice for Third-Party Components 12 | 13 | ZTNET utilizes the mkworld tool, written in Go, to generate the custom planet file. While the original mkworld tool was developed by ZeroTier, the version we are using was adapted and re-implemented in Go by Patrick Young (@kmahyyg). This Go adaptation is licensed under the GNU General Public License v3.0. We would like to express our appreciation to Patrick Young (@kmahyyg) for his efforts in creating this Go version, which has benefited our project. 14 | 15 | Our project, in its entirety, is also licensed under the GNU General Public License v3.0. For a comprehensive understanding of our project's licensing terms, please consult our LICENSE file. 16 | -------------------------------------------------------------------------------- /docs/docs/Licensing Notice/ztnet.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: ztnet 3 | title: License Notice 4 | slug: /licensing/ztnet 5 | description: ZTNET Licensing Notice - Detailed information on the GNU General Public License v3.0 (GPL-3.0) for ZTNET, including usage rights, obligations, and the full license text link. 6 | sidebar_position: 3 7 | --- 8 | 9 | # ZTNET 10 | 11 | ## General Information 12 | 13 | ZTNET is licensed under the GNU General Public License v3.0 (GPL-3.0). This means that you are free to modify and redistribute the source code under the terms of this license. 14 | 15 | ## What You Can Do 16 | 17 | - **Freedom to Use**: You can use the software for any purpose. 18 | - **Freedom to Examine**: You can study how the software works and modify it. 19 | - **Freedom to Share**: You can copy and distribute the software. 20 | - **Freedom to Improve**: You can enhance the software, and share your enhancements so that others may benefit. 21 | 22 | ## What You Must Do 23 | 24 | - **Share Changes**: If you modify ZTnet, you must release your changes under the same GPL-3.0 license. 25 | - **State Changes**: You must indicate what changes were made if you modify the source code. 26 | - **Keep Notices**: You must keep all copyright, patent, trademark, and attribution notices intact. 27 | 28 | ## Full License Text 29 | 30 | The full text of the GPL-3.0 license can be found [here](https://www.gnu.org/licenses/gpl-3.0.en.html). 31 | 32 | ## Disclaimer 33 | 34 | Please note that ZTnet is provided "as is" without warranty of any kind, either expressed or implied, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose. 35 | -------------------------------------------------------------------------------- /docs/docs/Rest Api/.gitignore: -------------------------------------------------------------------------------- 1 | *.mdx 2 | *.js -------------------------------------------------------------------------------- /docs/docs/Rest Api/Application/_source/stats.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: ZTNet Statistics Rest API 4 | # version: 1.0.0 5 | description: | 6 | Access the ZTNet Statistics API to get various statistics that can be used in 3rd party applications. 7 | Available from version v0.6.4. 8 | 9 | This interface is subject to a rate limit of 50 requests per minute to ensure service reliability. 10 | 11 | servers: 12 | - url: https://ztnet.network/api/v1 13 | description: ZTNet API 14 | variables: 15 | version: 16 | default: v1 17 | description: API version 18 | 19 | components: 20 | $ref: '../../_schema/security.yml#/components' 21 | 22 | security: 23 | - x-ztnet-auth: [] 24 | 25 | paths: 26 | /stats: 27 | get: 28 | summary: Returns statistics for ztnet 29 | description: | 30 | Returns various statistics that can be used in 3rd party applications. 31 | Available from version v0.6.4 32 | operationId: getAppStats 33 | responses: 34 | 200: 35 | description: An object of statistics 36 | content: 37 | application/json: 38 | schema: 39 | $ref: '../../_schema/StatsSchema.yml#/StatsResponse' 40 | example: 41 | $ref: '../../_example/StatsExample.yml#/StatsExample' 42 | 401: 43 | $ref: '../../_http_responses/Unauthorized.yml#/Unauthorized' 44 | 429: 45 | $ref: '../../_http_responses/RateLimitExceeded.yml#/RateLimitExceeded' 46 | 500: 47 | $ref: '../../_http_responses/InternalServerError.yml#/InternalServerError' -------------------------------------------------------------------------------- /docs/docs/Rest Api/Organization/Organization/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Organization", 3 | "position": 2, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } -------------------------------------------------------------------------------- /docs/docs/Rest Api/Organization/Users/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Users", 3 | "position": 1, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } -------------------------------------------------------------------------------- /docs/docs/Rest Api/Organization/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Organization", 3 | "position": 1, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } -------------------------------------------------------------------------------- /docs/docs/Rest Api/Organization/_source/users.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.1.0 2 | info: 3 | title: ZTNet Organization User Rest API 4 | # version: 1.0.0 5 | description: | 6 | The official ZTNet Organization Web API, provides public access with a rate limit of 50 requests per minute to maintain optimal service performance. 7 | servers: 8 | - url: https://ztnet.network/api/v1 9 | description: ZTNet API 10 | variables: 11 | version: 12 | default: v1 13 | description: API version 14 | tags: 15 | - name: organization_users 16 | description: Organization Users API 17 | x-displayName: Organization Users 18 | 19 | components: 20 | $ref: '../../_schema/security.yml#/components' 21 | 22 | security: 23 | - x-ztnet-auth: [] 24 | 25 | paths: 26 | /org/:orgid/user: 27 | get: 28 | tags: 29 | - organization_users 30 | summary: Returns a list of Users in the organization 31 | operationId: getOrganizationUsers 32 | parameters: 33 | - name: orgid 34 | in: path 35 | required: true 36 | schema: 37 | type: string 38 | description: Unique identifier of the organization 39 | responses: 40 | 200: 41 | description: An array of User IDs 42 | content: 43 | application/json: 44 | schema: 45 | $ref: '../../_schema/UserSchema.yml#/UserResponse' 46 | example: 47 | $ref: '../../_example/UserExample.yml#/UserExample' 48 | 401: 49 | $ref: '../../_http_responses/Unauthorized.yml#/Unauthorized' 50 | 429: 51 | $ref: '../../_http_responses/RateLimitExceeded.yml#/RateLimitExceeded' 52 | 500: 53 | $ref: '../../_http_responses/InternalServerError.yml#/InternalServerError' -------------------------------------------------------------------------------- /docs/docs/Rest Api/Personal/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Personal", 3 | "position": 1, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } -------------------------------------------------------------------------------- /docs/docs/Rest Api/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Rest API", 3 | "position": 4, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } -------------------------------------------------------------------------------- /docs/docs/Rest Api/_example/OrganizationExample.yml: -------------------------------------------------------------------------------- 1 | OrganizationExample: 2 | - id: "aa00abcdefghijjkl" 3 | orgName: "My Awesome Organization" 4 | ownerId: "aa10abcdefghijjkl" 5 | description: "" 6 | createdAt: "2024-03-11T21:18:00.524Z" 7 | - id: "aa01abcdefghijjkl" 8 | orgName: "Another Awesome Organization" 9 | ownerId: "aa10abcdefghijjkl" 10 | description: "" 11 | createdAt: "2024-03-12T15:19:44.799Z" 12 | 13 | OrganizationByIdExample: 14 | id: "dummy12345" 15 | name: "Awesome Ztnet Organization" 16 | createdAt: "2024-03-11T21:18:00.524Z" 17 | ownerId: "dummyOwner12345" 18 | networks: 19 | - nwid: "8056c2e21c000001" 20 | name: "ztnetNetworkName1" 21 | - nwid: "8056c2e21c000001" 22 | name: "ztnetNetworkName2" 23 | -------------------------------------------------------------------------------- /docs/docs/Rest Api/_example/StatsExample.yml: -------------------------------------------------------------------------------- 1 | StatsExample: 2 | - users: 1 3 | networks: 8 4 | networkMembers: 7 5 | appVersion: "v0.6.3" 6 | loginsLast24h: 1 7 | pendingUserInvitations: 0 8 | activeWebhooks: 0 9 | ztnetUptime: 735.491790795 10 | registrationEnabled: false 11 | hasPrivatRoot: true -------------------------------------------------------------------------------- /docs/docs/Rest Api/_example/UserExample.yml: -------------------------------------------------------------------------------- 1 | UserExample: 2 | - orgId: "aa01abcdefghijjkl" 3 | userId: "cltnemyox00004vypy4u69r5x" 4 | name: "John Doe" 5 | email: "johndoe@ztnet.network" 6 | role: "USER" 7 | - orgId: "aa01abcdefghijjkm" 8 | userId: "cltnemyox00004vypy4u69r5y" 9 | name: "Jane Doe" 10 | email: "janedoe@ztnet.network" 11 | role: "ADMIN" -------------------------------------------------------------------------------- /docs/docs/Rest Api/_http_responses/InternalServerError.yml: -------------------------------------------------------------------------------- 1 | InternalServerError: 2 | description: Internal Server Error, indicating that the server encountered an unexpected condition that prevented it from fulfilling the request. 3 | content: 4 | application/json: 5 | schema: 6 | type: object 7 | properties: 8 | error: 9 | type: string 10 | description: A message detailing the unexpected condition encountered by the server. 11 | example: 'Internal server error. Please try again later.' 12 | examples: 13 | internalServerErrorExample: 14 | summary: Example response for an internal server error 15 | value: 16 | error: 'Internal server error. Please try again later.' 17 | -------------------------------------------------------------------------------- /docs/docs/Rest Api/_http_responses/RateLimitExceeded.yml: -------------------------------------------------------------------------------- 1 | RateLimitExceeded: 2 | description: Rate limit exceeded, indicating that the request was not accepted because the application has exceeded the rate limit for the API. 3 | content: 4 | application/json: 5 | schema: 6 | type: object 7 | properties: 8 | error: 9 | type: string 10 | description: A message detailing the reason for exceeding the rate limit. 11 | example: 'Rate limit exceeded. Try again in X minutes.' 12 | examples: 13 | rateLimitExceededExample: 14 | summary: Example response for rate limit exceeded 15 | value: 16 | error: 'Rate limit exceeded. Try again in 1 minute.' -------------------------------------------------------------------------------- /docs/docs/Rest Api/_http_responses/Unauthorized.yml: -------------------------------------------------------------------------------- 1 | Unauthorized: 2 | description: Unauthorized access, indicating that the request has not been applied because it lacks valid authentication credentials for the target resource. 3 | content: 4 | application/json: 5 | schema: 6 | type: object 7 | properties: 8 | error: 9 | type: string 10 | description: A message detailing the reason for the unauthorized status. 11 | example: 'Unauthorized: API key is missing or invalid.' 12 | examples: 13 | missingApiKey: 14 | summary: API key is missing 15 | value: 16 | error: 'Unauthorized: API key is missing.' 17 | invalidApiKey: 18 | summary: API key is invalid 19 | value: 20 | error: 'Unauthorized: API key is invalid.' 21 | -------------------------------------------------------------------------------- /docs/docs/Rest Api/_schema/OrganizationSchema.yml: -------------------------------------------------------------------------------- 1 | Organization: 2 | type: object 3 | properties: 4 | id: 5 | type: string 6 | example: cltng0000000abcdefghijjkl 7 | description: Unique identifier for the organization. 8 | orgName: 9 | type: string 10 | example: My Awesome Organization 11 | description: Name of the organization. 12 | ownerId: 13 | type: string 14 | example: cltng0000001abcdefghijjkl 15 | description: Unique identifier for the owner of the organization. 16 | description: 17 | type: string 18 | example: '' 19 | description: A brief description of the organization. 20 | createdAt: 21 | type: string 22 | format: date-time 23 | example: '2024-03-11T21:18:00.524Z' 24 | description: The ISO 8601 date format of the time that the organization was created. 25 | 26 | OrganizationResponse: 27 | type: array 28 | items: 29 | $ref: '#/Organization' 30 | 31 | OrganizationById: 32 | type: object 33 | properties: 34 | id: 35 | type: string 36 | example: "dummy12345id" 37 | description: Unique identifier for the organization. 38 | name: 39 | type: string 40 | example: "Awesome Egeland Test" 41 | description: Name of the organization. 42 | createdAt: 43 | type: string 44 | format: date-time 45 | example: "2024-03-11T21:18:00.524Z" 46 | description: The ISO 8601 date format of the time that the organization was created. 47 | ownerId: 48 | type: string 49 | example: "dummyOwner12345" 50 | description: Unique identifier for the owner of the organization. 51 | networks: 52 | type: array 53 | items: 54 | type: object 55 | properties: 56 | nwid: 57 | type: string 58 | example: "dummyNetwork12345" 59 | description: Unique network identifier within the organization. 60 | name: 61 | type: string 62 | example: "Awesome Uncertain Butterfly" 63 | description: Name of the network. 64 | 65 | -------------------------------------------------------------------------------- /docs/docs/Rest Api/_schema/StatsSchema.yml: -------------------------------------------------------------------------------- 1 | StatsResponse: 2 | type: object 3 | properties: 4 | users: 5 | type: integer 6 | description: The number of users. 7 | networks: 8 | type: integer 9 | description: The number of networks. 10 | networkMembers: 11 | type: integer 12 | description: The total number of network members. 13 | appVersion: 14 | type: string 15 | description: The current version of the ztnet application. 16 | loginsLast24h: 17 | type: integer 18 | description: The number of logins in the last 24 hours. 19 | pendingUserInvitations: 20 | type: integer 21 | description: The number of pending user invitations. 22 | activeWebhooks: 23 | type: integer 24 | description: The number of active webhooks. 25 | ztnetUptime: 26 | type: number 27 | format: float 28 | description: The uptime of ztnet in minutes. 29 | registrationEnabled: 30 | type: boolean 31 | description: Indicates if registration is enabled. 32 | hasPrivatRoot: 33 | type: boolean 34 | description: Indicates if a private root is currently in use. -------------------------------------------------------------------------------- /docs/docs/Rest Api/_schema/UserSchema.yml: -------------------------------------------------------------------------------- 1 | User: 2 | type: object 3 | properties: 4 | orgId: 5 | type: string 6 | example: "aa01abcdefghijjkl" 7 | description: Unique identifier for the organization to which the user belongs. 8 | userId: 9 | type: string 10 | example: "aa01abcdefghijjkm" 11 | description: Unique identifier for the user. 12 | name: 13 | type: string 14 | example: "John Doe" 15 | description: Name of the user. 16 | email: 17 | type: string 18 | example: "johndoe@ztnet.network" 19 | description: Email address of the user. 20 | role: 21 | type: string 22 | example: "USER" 23 | description: Role of the user within the organization. 24 | 25 | UserResponse: 26 | type: array 27 | items: 28 | $ref: '#/User' 29 | -------------------------------------------------------------------------------- /docs/docs/Rest Api/_schema/security.yml: -------------------------------------------------------------------------------- 1 | components: 2 | securitySchemes: 3 | x-ztnet-auth: 4 | type: apiKey 5 | in: header 6 | name: x-ztnet-auth 7 | description: API key required for access -------------------------------------------------------------------------------- /docs/docs/Showcase/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Showcase", 3 | "position": 6, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } -------------------------------------------------------------------------------- /docs/docs/Showcase/images.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: app_images 3 | title: ZTNet Images 4 | slug: /showcase/images 5 | description: Explore the visual gallery of ZTNET - Images and screenshots highlighting features and user interface. 6 | --- 7 | 8 | # Images 9 | Discover the functionality and design of the ZTNet application through this collection of images, providing a comprehensive visual guide to its features and user experience. 10 | 11 | ### Organization Layout 12 | ![Networks](../../images/showcase/organization_layout.jpg) 13 | 14 | ### Network Page 15 | ![Networks](../../images/showcase/network_local.jpg) 16 | 17 | ### Network Member Options 18 | ![Networks](../../images/showcase/member_options.jpg) 19 | 20 | ### Mail Settings 21 | ![Networks](../../images/showcase/admin_mail.jpg) 22 | 23 | ### Application Users 24 | ![Networks](../../images/showcase/admin_users.jpg) 25 | 26 | ### Controller 27 | ![Networks](../../images/showcase/admin_controller.jpg) 28 | 29 | ### User Profile 30 | ![Networks](../../images/showcase/profile.jpg) -------------------------------------------------------------------------------- /docs/docs/Usage/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Configuration & Tools", 3 | "position": 5, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } -------------------------------------------------------------------------------- /docs/docs/_tutorial-basics/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Tutorial - Basics", 3 | "position": 2, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "5 minutes to learn the most important Docusaurus concepts." 7 | } 8 | } -------------------------------------------------------------------------------- /docs/docs/_tutorial-basics/congratulations.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | --- 4 | 5 | # Congratulations! 6 | 7 | You have just learned the **basics of Docusaurus** and made some changes to the **initial template**. 8 | 9 | Docusaurus has **much more to offer**! 10 | 11 | Have **5 more minutes**? Take a look at **[versioning](../tutorial-extras/manage-docs-versions.md)** and **[i18n](../tutorial-extras/translate-your-site.md)**. 12 | 13 | Anything **unclear** or **buggy** in this tutorial? [Please report it!](https://github.com/facebook/docusaurus/discussions/4610) 14 | 15 | ## What's next? 16 | 17 | - Read the [official documentation](https://docusaurus.io/) 18 | - Modify your site configuration with [`docusaurus.config.js`](https://docusaurus.io/docs/api/docusaurus-config) 19 | - Add navbar and footer items with [`themeConfig`](https://docusaurus.io/docs/api/themes/configuration) 20 | - Add a custom [Design and Layout](https://docusaurus.io/docs/styling-layout) 21 | - Add a [search bar](https://docusaurus.io/docs/search) 22 | - Find inspirations in the [Docusaurus showcase](https://docusaurus.io/showcase) 23 | - Get involved in the [Docusaurus Community](https://docusaurus.io/community/support) 24 | -------------------------------------------------------------------------------- /docs/docs/_tutorial-basics/create-a-blog-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Create a Blog Post 6 | 7 | Docusaurus creates a **page for each blog post**, but also a **blog index page**, a **tag system**, an **RSS** feed... 8 | 9 | ## Create your first Post 10 | 11 | Create a file at `blog/2021-02-28-greetings.md`: 12 | 13 | ```md title="blog/2021-02-28-greetings.md" 14 | --- 15 | slug: greetings 16 | title: Greetings! 17 | authors: 18 | - name: Joel Marcey 19 | title: Co-creator of Docusaurus 1 20 | url: https://github.com/JoelMarcey 21 | image_url: https://github.com/JoelMarcey.png 22 | - name: Sébastien Lorber 23 | title: Docusaurus maintainer 24 | url: https://sebastienlorber.com 25 | image_url: https://github.com/slorber.png 26 | tags: [greetings] 27 | --- 28 | 29 | Congratulations, you have made your first post! 30 | 31 | Feel free to play around and edit this post as much you like. 32 | ``` 33 | 34 | A new blog post is now available at [http://localhost:3000/blog/greetings](http://localhost:3000/blog/greetings). 35 | -------------------------------------------------------------------------------- /docs/docs/_tutorial-basics/create-a-document.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Create a Document 6 | 7 | Documents are **groups of pages** connected through: 8 | 9 | - a **sidebar** 10 | - **previous/next navigation** 11 | - **versioning** 12 | 13 | ## Create your first Doc 14 | 15 | Create a Markdown file at `docs/hello.md`: 16 | 17 | ```md title="docs/hello.md" 18 | # Hello 19 | 20 | This is my **first Docusaurus document**! 21 | ``` 22 | 23 | A new document is now available at [http://localhost:3000/docs/hello](http://localhost:3000/docs/hello). 24 | 25 | ## Configure the Sidebar 26 | 27 | Docusaurus automatically **creates a sidebar** from the `docs` folder. 28 | 29 | Add metadata to customize the sidebar label and position: 30 | 31 | ```md title="docs/hello.md" {1-4} 32 | --- 33 | sidebar_label: 'Hi!' 34 | sidebar_position: 3 35 | --- 36 | 37 | # Hello 38 | 39 | This is my **first Docusaurus document**! 40 | ``` 41 | 42 | It is also possible to create your sidebar explicitly in `sidebars.js`: 43 | 44 | ```js title="sidebars.js" 45 | module.exports = { 46 | tutorialSidebar: [ 47 | 'intro', 48 | // highlight-next-line 49 | 'hello', 50 | { 51 | type: 'category', 52 | label: 'Tutorial', 53 | items: ['tutorial-basics/create-a-document'], 54 | }, 55 | ], 56 | }; 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/docs/_tutorial-basics/create-a-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Create a Page 6 | 7 | Add **Markdown or React** files to `src/pages` to create a **standalone page**: 8 | 9 | - `src/pages/index.js` → `localhost:3000/` 10 | - `src/pages/foo.md` → `localhost:3000/foo` 11 | - `src/pages/foo/bar.js` → `localhost:3000/foo/bar` 12 | 13 | ## Create your first React Page 14 | 15 | Create a file at `src/pages/my-react-page.js`: 16 | 17 | ```jsx title="src/pages/my-react-page.js" 18 | import React from 'react'; 19 | import Layout from '@theme/Layout'; 20 | 21 | export default function MyReactPage() { 22 | return ( 23 | 24 |

My React page

25 |

This is a React page

26 |
27 | ); 28 | } 29 | ``` 30 | 31 | A new page is now available at [http://localhost:3000/my-react-page](http://localhost:3000/my-react-page). 32 | 33 | ## Create your first Markdown Page 34 | 35 | Create a file at `src/pages/my-markdown-page.md`: 36 | 37 | ```mdx title="src/pages/my-markdown-page.md" 38 | # My Markdown page 39 | 40 | This is a Markdown page 41 | ``` 42 | 43 | A new page is now available at [http://localhost:3000/my-markdown-page](http://localhost:3000/my-markdown-page). 44 | -------------------------------------------------------------------------------- /docs/docs/_tutorial-basics/deploy-your-site.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # Deploy your site 6 | 7 | Docusaurus is a **static-site-generator** (also called **[Jamstack](https://jamstack.org/)**). 8 | 9 | It builds your site as simple **static HTML, JavaScript and CSS files**. 10 | 11 | ## Build your site 12 | 13 | Build your site **for production**: 14 | 15 | ```bash 16 | npm run build 17 | ``` 18 | 19 | The static files are generated in the `build` folder. 20 | 21 | ## Deploy your site 22 | 23 | Test your production build locally: 24 | 25 | ```bash 26 | npm run serve 27 | ``` 28 | 29 | The `build` folder is now served at [http://localhost:3000/](http://localhost:3000/). 30 | 31 | You can now deploy the `build` folder **almost anywhere** easily, **for free** or very small cost (read the **[Deployment Guide](https://docusaurus.io/docs/deployment)**). 32 | -------------------------------------------------------------------------------- /docs/docs/_usage/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Usage", 3 | "position": 2, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } -------------------------------------------------------------------------------- /docs/docs/_usage/import_controller.md: -------------------------------------------------------------------------------- 1 | # Migrate Controller 2 | ## How to Migrate an Established ZeroTier Controller into ZTNET 3 | 4 | To move an already established ZeroTier controller into a Docker container, you can follow these steps. This guide assumes you have Docker set up on the machine where the new controller will reside. 5 | 6 | ### Prerequisites 7 | - Ensure Docker is installed and running. 8 | - Stop the ZeroTier service on the source controller to prevent data inconsistencies. 9 | 10 | ### Steps 11 | 12 | 1. **Stop the ZeroTier Service on the Source Machine** 13 | Depending on your OS, use the relevant command to stop the ZeroTier service. 14 | ```bash 15 | sudo systemctl stop zerotier-one # For systemd-based Linux distributions 16 | ``` 17 | Or for other systems: 18 | ```bash 19 | sudo service zerotier-one stop # For SysVinit or Upstart 20 | ``` 21 | 22 | 2. **Locate the ZeroTier Configuration Folder** 23 | The configuration folder is typically `/var/lib/zerotier-one` on Linux systems. 24 | 25 | 3. **Backup Configuration** 26 | Create a backup just to be safe. 27 | ```bash 28 | sudo cp -r /var/lib/zerotier-one /path/to/backup/folder 29 | ``` 30 | 31 | 4. **Copy Configuration to Docker Volume** 32 | Copy the configuration files to the Docker volume specified in your Docker Compose file. Replace `zerotier:/var/lib/zerotier-one` with the actual volume name and path. 33 | ```bash 34 | sudo cp -r /var/lib/zerotier-one /path/to/docker/volume 35 | ``` 36 | 37 | 5. **Start the Docker Container** 38 | Use Docker Compose to start your new ZeroTier controller. Navigate to the directory where your `docker-compose.yml` file is located. 39 | ```bash 40 | docker-compose up -d 41 | ``` 42 | 43 | 6. **Verify the Controller** 44 | Verify that the new Dockerized ZeroTier controller has all the networks and members from the source controller. 45 | -------------------------------------------------------------------------------- /docs/ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: 'ztnet.network', 5 | script: 'npm run serve -- --dir /home/sinamics/ztnet_docs/build --port 3000 --host 0.0.0.0', 6 | log_date_format: 'YYYY-MM-DD HH:mm Z', 7 | env: { 8 | NODE_ENV: 'production', 9 | }, 10 | }, 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /docs/images/logo/old/ztnet_100x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/logo/old/ztnet_100x100.png -------------------------------------------------------------------------------- /docs/images/logo/old/ztnet_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/logo/old/ztnet_16x16.png -------------------------------------------------------------------------------- /docs/images/logo/old/ztnet_200x200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/logo/old/ztnet_200x200.png -------------------------------------------------------------------------------- /docs/images/logo/old/ztnet_original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/logo/old/ztnet_original.png -------------------------------------------------------------------------------- /docs/images/logo/old/ztnet_social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/logo/old/ztnet_social.png -------------------------------------------------------------------------------- /docs/images/logo/ztnet_100x89.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/logo/ztnet_100x89.png -------------------------------------------------------------------------------- /docs/images/logo/ztnet_16x14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/logo/ztnet_16x14.png -------------------------------------------------------------------------------- /docs/images/logo/ztnet_200x178.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/logo/ztnet_200x178.png -------------------------------------------------------------------------------- /docs/images/logo/ztnet_300x267.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/logo/ztnet_300x267.png -------------------------------------------------------------------------------- /docs/images/logo/ztnet_300x300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/logo/ztnet_300x300.png -------------------------------------------------------------------------------- /docs/images/logo/ztnet_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/logo/ztnet_512x512.png -------------------------------------------------------------------------------- /docs/images/logo/ztnet_orginal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/logo/ztnet_orginal.png -------------------------------------------------------------------------------- /docs/images/logo/ztnet_social.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/logo/ztnet_social.jpg -------------------------------------------------------------------------------- /docs/images/logo/ztnet_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/logo/ztnet_test.png -------------------------------------------------------------------------------- /docs/images/showcase/admin_controller.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/showcase/admin_controller.jpg -------------------------------------------------------------------------------- /docs/images/showcase/admin_mail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/showcase/admin_mail.jpg -------------------------------------------------------------------------------- /docs/images/showcase/admin_users.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/showcase/admin_users.jpg -------------------------------------------------------------------------------- /docs/images/showcase/member_options.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/showcase/member_options.jpg -------------------------------------------------------------------------------- /docs/images/showcase/members_table.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/showcase/members_table.jpg -------------------------------------------------------------------------------- /docs/images/showcase/network_local.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/showcase/network_local.jpg -------------------------------------------------------------------------------- /docs/images/showcase/network_page.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/showcase/network_page.jpg -------------------------------------------------------------------------------- /docs/images/showcase/organization_layout.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/showcase/organization_layout.jpg -------------------------------------------------------------------------------- /docs/images/showcase/profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/showcase/profile.jpg -------------------------------------------------------------------------------- /docs/images/webhooks/zapier_actions.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/webhooks/zapier_actions.jpg -------------------------------------------------------------------------------- /docs/images/webhooks/zapier_hook.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/webhooks/zapier_hook.jpg -------------------------------------------------------------------------------- /docs/images/ztnet_logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/ztnet_logo.psd -------------------------------------------------------------------------------- /docs/images/ztnet_social.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/images/ztnet_social.psd -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ztnet", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start --host 10.0.0.217 --port 4000", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "2.2.0", 19 | "@docusaurus/preset-classic": "2.2.0", 20 | "@mdx-js/react": "^1.6.22", 21 | "@stackql/docusaurus-plugin-structured-data": "^1.3.2", 22 | "clsx": "^1.2.1", 23 | "docusaurus-plugin-openapi-docs": "1.7.3", 24 | "docusaurus-theme-openapi-docs": "1.7.3", 25 | "prism-react-renderer": "^1.3.5", 26 | "react": "^17.0.2", 27 | "react-dom": "^17.0.2" 28 | }, 29 | "devDependencies": { 30 | "@docusaurus/module-type-aliases": "2.4.0", 31 | "@tsconfig/docusaurus": "^1.0.5", 32 | "typescript": "^4.7.4" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.5%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "engines": { 47 | "node": ">=16.14" 48 | } 49 | } -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 18 | // gettingStartedSidebar: [{type: 'autogenerated', dirName: '.'}], 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | 'intro', 23 | 'hello', 24 | { 25 | type: 'category', 26 | label: 'Tutorial', 27 | items: ['tutorial-basics/create-a-document'], 28 | }, 29 | ], 30 | */ 31 | }; 32 | 33 | module.exports = sidebars; 34 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import clsx from "clsx"; 3 | import styles from "./styles.module.css"; 4 | 5 | type FeatureItem = { 6 | title: string; 7 | Svg: React.ComponentType>; 8 | description: JSX.Element; 9 | }; 10 | 11 | const FeatureList: FeatureItem[] = [ 12 | { 13 | title: "Easy to Use", 14 | Svg: require("@site/static/img/undraw_docusaurus_mountain.svg").default, 15 | description: ( 16 | <> 17 | Docusaurus was designed from the ground up to be easily installed and used to get 18 | your website up and running quickly. 19 | 20 | ), 21 | }, 22 | { 23 | title: "Focus on What Matters", 24 | Svg: require("@site/static/img/undraw_docusaurus_tree.svg").default, 25 | description: ( 26 | <> 27 | Docusaurus lets you focus on your docs, and we'll do the chores. Go ahead and 28 | move your docs into the docs directory. 29 | 30 | ), 31 | }, 32 | { 33 | title: "Powered by React", 34 | Svg: require("@site/static/img/undraw_docusaurus_react.svg").default, 35 | description: ( 36 | <> 37 | Extend or customize your website layout by reusing React. Docusaurus can be 38 | extended while reusing the same header and footer. 39 | 40 | ), 41 | }, 42 | ]; 43 | 44 | function Feature({ title, Svg, description }: FeatureItem) { 45 | return ( 46 |
47 |
48 | 49 |
50 |
51 |

{title}

52 |

{description}

53 |
54 |
55 | ); 56 | } 57 | 58 | export default function HomepageFeatures(): JSX.Element { 59 | return ( 60 |
61 |
62 |
63 | {FeatureList.map((props, idx) => ( 64 | // biome-ignore lint/suspicious/noArrayIndexKey: 65 | 66 | ))} 67 |
68 |
69 |
70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /docs/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/static/.nojekyll -------------------------------------------------------------------------------- /docs/static/google46bf74768b883ad6.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google46bf74768b883ad6.html -------------------------------------------------------------------------------- /docs/static/img/admin/controller/unlinked_networks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/static/img/admin/controller/unlinked_networks.png -------------------------------------------------------------------------------- /docs/static/img/admin/controller/zerotier_api_url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/static/img/admin/controller/zerotier_api_url.png -------------------------------------------------------------------------------- /docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /docs/static/img/member_options.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/static/img/member_options.jpg -------------------------------------------------------------------------------- /docs/static/img/network_page.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/static/img/network_page.jpg -------------------------------------------------------------------------------- /docs/static/img/usage/vpn_passthrough.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/static/img/usage/vpn_passthrough.png -------------------------------------------------------------------------------- /docs/static/img/ztnet_100x89.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/static/img/ztnet_100x89.png -------------------------------------------------------------------------------- /docs/static/img/ztnet_landing.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/static/img/ztnet_landing.jpg -------------------------------------------------------------------------------- /docs/static/img/ztnet_landing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/static/img/ztnet_landing.png -------------------------------------------------------------------------------- /docs/static/img/ztnet_social.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/docs/static/img/ztnet_social.jpg -------------------------------------------------------------------------------- /docs/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@tsconfig/docusaurus/tsconfig.json", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } -------------------------------------------------------------------------------- /init-db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Enable error handling and debug tracing 4 | set -e 5 | # set -x ( DEBUG ) 6 | 7 | error_handling() { 8 | echo "An error occurred. Exiting..." 9 | exit 1 10 | } 11 | # trap errors 12 | trap error_handling ERR 13 | 14 | cmd="$@" 15 | 16 | # Create .env file 17 | echo "Creating .env file..." 18 | cat << EOF > .env 19 | DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}?schema=public 20 | ZT_ADDR=${ZT_ADDR} 21 | NEXT_PUBLIC_APP_VERSION=${NEXT_PUBLIC_APP_VERSION} 22 | EOF 23 | 24 | # config 25 | envFilename='.env' 26 | nextFolder='.next' 27 | 28 | # Currently not in use. 29 | function apply_path { 30 | # echo "Applying path..." 31 | while read line; do 32 | if [ "${line:0:1}" == "#" ] || [ "${line}" == "" ]; then 33 | continue 34 | fi 35 | configName="$(cut -d'=' -f1 <<<"$line")" 36 | configValue="$(cut -d'=' -f2 <<<"$line")" 37 | envValue="${!configName}"; 38 | 39 | if [ -n "$configValue" ] && [ -n "$envValue" ]; then 40 | echo "Replace: ${configValue} with: ${envValue}" 41 | find $nextFolder \( -type d -name .git -prune \) -o -type f -print0 | xargs -0 sed -i "s#$configValue#$envValue#g" 42 | fi 43 | done < $envFilename 44 | } 45 | 46 | # apply_path 47 | 48 | until PGPASSWORD=$POSTGRES_PASSWORD psql -h "$POSTGRES_HOST" -p "$POSTGRES_PORT" -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c '\q'; do 49 | echo "Postgres is unavailable - sleeping" 50 | sleep 1 51 | done 52 | 53 | # apply migrations to the database 54 | echo "Applying migrations to the database..." 55 | npx prisma migrate deploy 56 | echo "Migrations applied successfully!" 57 | 58 | # seed the database 59 | echo "Seeding the database..." 60 | npx prisma db seed 61 | echo "Database seeded successfully!" 62 | 63 | echo "Executing command" 64 | exec $cmd 65 | -------------------------------------------------------------------------------- /install.ztnet/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | dist 4 | bin/ -------------------------------------------------------------------------------- /install.ztnet/README.md: -------------------------------------------------------------------------------- 1 | ## ztnet installation script 2 | 3 | This is the code and installation scripts running at install.ztnet.network. 4 | 5 | ### Installation Steps for Debian and Ubuntu 6 | 7 | 1. Open a terminal window. 8 | 2. Install curl if it is not already installed: 9 | ```bash 10 | sudo apt update && sudo apt install curl 11 | ``` 12 | 3. Run the following command to download and execute the installation script: 13 | 14 | **!NOTE:** if you system does not have sudo installed, you will need to run the script as root and remove the sudo from the command below. 15 | 16 | ```bash 17 | curl -s http://install.ztnet.network | sudo bash 18 | ``` 19 | 20 | 4. Follow any on-screen instructions to complete the installation. 21 | 22 | After completing these steps, ztnet should be successfully installed on your system. 23 | 24 | ### Running the Server (Development) 25 | 26 | To run the server, follow these steps: 27 | 28 | 1. Install the dependencies: 29 | 30 | ```bash 31 | npm install 32 | ``` 33 | 34 | 2. To start the server in development mode, run: 35 | 36 | ```bash 37 | npm run start 38 | ``` 39 | 40 | 3. To build the project, run: 41 | ```bash 42 | npm run build 43 | ``` 44 | 45 | These commands are specified in the `package.json` under the `scripts` section. 46 | -------------------------------------------------------------------------------- /install.ztnet/bash/error.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | YELLOW='\033[1;33m' 3 | NC='\033[0m' # No Color 4 | 5 | printf "\n${YELLOW}Could not find any ztnet installation file, please make sure you typed the command correctly!${NC}\n\n" 6 | printf "${YELLOW}please refer to https://docs.ztnet.network/${NC}\n\n" -------------------------------------------------------------------------------- /install.ztnet/ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: 'ztnet_installer', 5 | script: './dist/index.js', 6 | log_date_format: 'YYYY-MM-DD HH:mm Z', 7 | env: { 8 | NODE_ENV: 'production', 9 | }, 10 | }, 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /install.ztnet/nginx/install.ztnet.conf: -------------------------------------------------------------------------------- 1 | upstream backend { 2 | server localhost:9090; #node app address 3 | } 4 | # redirect https => http 5 | server { 6 | listen 443 ssl; 7 | server_name install.ztnet.network; 8 | return 301 http://$host$request_uri; 9 | } 10 | server { 11 | listen 80; 12 | server_name install.ztnet.network; 13 | 14 | location / { 15 | add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive"; 16 | add_header X-Frame-Options SAMEORIGIN; 17 | add_header X-Content-Type-Options nosniff; 18 | add_header X-XSS-Protection "1; mode=block"; 19 | proxy_pass http://backend; 20 | proxy_http_version 1.1; 21 | proxy_set_header Upgrade $http_upgrade; 22 | proxy_set_header X-Forwarded-For $remote_addr; 23 | proxy_set_header Connection 'upgrade'; 24 | proxy_set_header Host $host; 25 | proxy_cache_bypass $http_upgrade; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /install.ztnet/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "install.ztnet.network", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "ts-node-dev --respawn --transpile-only src/index.ts", 8 | "build": "tsc" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "dotenv": "^16.0.1", 14 | "express": "^4.21.2", 15 | "express-rate-limit": "^6.4.0", 16 | "mime": "^3.0.0", 17 | "node-fetch": "^2.6.11", 18 | "nodemailer": "^6.9.9", 19 | "ts-node-dev": "^2.0.0", 20 | "uuidv4": "^6.2.13" 21 | }, 22 | "devDependencies": { 23 | "@types/express": "^4.17.13", 24 | "@types/node": "^17.0.32", 25 | "@types/node-fetch": "^2.6.4", 26 | "@types/nodemailer": "^6.4.4", 27 | "@types/universal-analytics": "^0.4.5" 28 | } 29 | } -------------------------------------------------------------------------------- /install.ztnet/src/project-config.ts: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | // export contstants 4 | export const PROJECT_DIR = path.join(__dirname, '..'); 5 | export const PROD_BINARIES_PATH = '/var/www/install.ztnet/bin'; 6 | export const DEV_BINARIES_PATH = path.join(__dirname, '..', 'bin'); 7 | -------------------------------------------------------------------------------- /install.ztnet/src/routes/getBashInstaller.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { Request, Response } from 'express'; 3 | import fs from 'fs'; 4 | import { removeTrailingSlash } from '../utils/helpers'; 5 | 6 | // Initialize a counter variable at the top level 7 | let downloadCounter = 0; 8 | 9 | //fetch bash script 10 | export const getBashInstaller = async function (req: Request, res: Response) { 11 | const url = removeTrailingSlash(req.url); 12 | const inst_path = !url ? `bash/ztnet.sh` : `bash${url}.sh`; 13 | 14 | const filename = 'install.sh'; 15 | const fileURL = path.join(__dirname, '..', '..', inst_path); 16 | const options = { 17 | headers: { 18 | 'x-timestamp': Date.now(), 19 | 'x-sent': true, 20 | 'content-disposition': 'attachment; filename=' + filename, 21 | }, 22 | }; 23 | 24 | //validate that file exists 25 | fs.access(fileURL, fs.constants.R_OK, (err) => { 26 | if (err) { 27 | console.error(err); 28 | res.download(path.join(__dirname, '..', '..', 'bash/error.sh'), 'error.sh', options); 29 | return; 30 | } 31 | 32 | // File exists, increment the download counter and log it with the current date 33 | downloadCounter++; 34 | const currentDate = new Date().toISOString(); 35 | console.log(`[${currentDate}] Bash file has been downloaded ${downloadCounter} times.`); 36 | 37 | //file exists 38 | res.download(fileURL, 'install.sh', options); 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /install.ztnet/src/routes/getBinary.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { Request, Response } from 'express'; 3 | import fs from 'fs'; 4 | import { PROD_BINARIES_PATH, DEV_BINARIES_PATH } from '../project-config'; 5 | 6 | //download binary 7 | export const getBinary = async function (req: Request, res: Response) { 8 | const folderPath = process.env.NODE_ENV !== 'development' ? PROD_BINARIES_PATH : DEV_BINARIES_PATH; 9 | 10 | const { arch, version, app }: any = req.query; 11 | 12 | if (!arch || !version || !app) 13 | return res.status(404).send('Need args: ?arch=armhf&app=ztnet&version="0.3.7"\n'); 14 | 15 | const binPath = path.join(folderPath, app, arch); 16 | 17 | let filesArr: any = []; 18 | 19 | try { 20 | filesArr = fs.readdirSync(binPath); 21 | } catch (error) { 22 | return res.download(path.join(__dirname, '..', '..', 'bash/error.sh'), 'error.sh'); 23 | } 24 | 25 | // sort array descending 26 | const sortedFiles = filesArr.sort((a: any, b: any) => { 27 | return b.localeCompare(a); 28 | }); 29 | 30 | // find filename inlcuding version number 31 | const file = sortedFiles.find((file: string) => { 32 | if (version === 'latest') { 33 | return sortedFiles[0]; 34 | } 35 | 36 | return file.includes(version); 37 | }); 38 | 39 | if (!file) return res.status(404).send('Version does not exist\n'); 40 | 41 | const filePath = path.join(binPath, file); 42 | 43 | const options = { 44 | headers: { 45 | 'x-timestamp': Date.now(), 46 | 'x-sent': true, 47 | }, 48 | }; 49 | 50 | //validate that file exists 51 | return fs.access(filePath, fs.constants.R_OK, (err) => { 52 | if (err) { 53 | console.error(err); 54 | return res.status(403).send('Status: File not found!\n'); 55 | } 56 | 57 | //file exists 58 | return res.download(filePath, file, options); 59 | }); 60 | }; 61 | -------------------------------------------------------------------------------- /install.ztnet/src/routes/health.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | 3 | //health status for uptime kuma. 4 | export const getHealth = async function(_: Request, res:Response) { 5 | return res.status(200).send({response: "ok"}); 6 | } -------------------------------------------------------------------------------- /install.ztnet/src/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | export const removeTrailingSlash = (str: string) => { 2 | return str.replace(/\/+$/, ''); 3 | }; 4 | -------------------------------------------------------------------------------- /install.ztnet/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "allowJs": true, 5 | "module": "commonjs", 6 | "lib": [ 7 | "dom", 8 | "es6", 9 | "es2017", 10 | "esnext.asynciterable" 11 | ], 12 | "sourceMap": true, 13 | "outDir": "./dist", 14 | "moduleResolution": "node", 15 | "removeComments": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "strictFunctionTypes": true, 19 | "noImplicitThis": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noImplicitReturns": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "allowSyntheticDefaultImports": true, 25 | "esModuleInterop": true, 26 | "emitDecoratorMetadata": true, 27 | "experimentalDecorators": true, 28 | "resolveJsonModule": true, 29 | "baseUrl": "." 30 | }, 31 | "exclude": [ 32 | "node_modules" 33 | ], 34 | "include": [ 35 | "src/**/*.ts", 36 | "src/**/*.js" 37 | ] 38 | } -------------------------------------------------------------------------------- /jest.api.config.ts: -------------------------------------------------------------------------------- 1 | import type { JestConfigWithTsJest } from "ts-jest"; 2 | import nextJest from "next/jest.js"; 3 | 4 | const nextConfig = { 5 | dir: "./", // Path to your Next.js app 6 | }; 7 | 8 | const baseConfig: JestConfigWithTsJest = { 9 | clearMocks: true, 10 | coverageProvider: "v8", 11 | preset: "ts-jest/presets/js-with-ts", 12 | transform: { 13 | "^.+\\.mjs$": "ts-jest", 14 | }, 15 | }; 16 | 17 | const filesConfig = { 18 | setupFiles: ["dotenv/config"], 19 | setupFilesAfterEnv: ["/jest.setup.ts"], 20 | }; 21 | 22 | const moduleConfig = { 23 | moduleNameMapper: { 24 | "^~/(.*)$": "/src/$1", 25 | }, 26 | }; 27 | 28 | const testConfig = { 29 | testMatch: [ 30 | "**/server/api/__tests__/**/*.test.ts", 31 | "**/pages/api/__tests__/**/*.test.ts", 32 | ], 33 | }; 34 | 35 | const jestConfig: JestConfigWithTsJest = { 36 | ...baseConfig, 37 | ...filesConfig, 38 | ...moduleConfig, 39 | ...testConfig, 40 | }; 41 | 42 | const createJestConfig = nextJest(nextConfig); 43 | 44 | // biome-ignore lint/suspicious/noExplicitAny: 45 | export default createJestConfig(jestConfig as any); 46 | -------------------------------------------------------------------------------- /jest.pages.config.ts: -------------------------------------------------------------------------------- 1 | import type { JestConfigWithTsJest } from "ts-jest"; 2 | // jest.config.mjs 3 | import nextJest from "next/jest.js"; 4 | 5 | const createJestConfig = nextJest({ 6 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 7 | dir: "./", 8 | }); 9 | const jestConfig: JestConfigWithTsJest = { 10 | clearMocks: true, 11 | coverageProvider: "v8", 12 | preset: "ts-jest/presets/js-with-ts", 13 | setupFiles: ["dotenv/config"], 14 | transform: { 15 | "^.+\\.mjs$": "ts-jest", 16 | }, 17 | setupFilesAfterEnv: ["/jest.setup.ts"], 18 | testEnvironment: "jest-environment-jsdom", 19 | moduleNameMapper: { 20 | "^~/(.*)$": "/src/$1", 21 | "^lib/(.*)$": "/common/$1", 22 | }, 23 | testMatch: ["**/__tests__/**/*.test.tsx"], 24 | }; 25 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument 26 | export default createJestConfig(jestConfig as any); 27 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom/extend-expect"; 2 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. 3 | * This is especially useful for Docker builds. 4 | */ 5 | !process.env.SKIP_ENV_VALIDATION && (await import("./src/env.mjs")); 6 | 7 | /** @type {import("next").NextConfig} */ 8 | const config = { 9 | reactStrictMode: true, 10 | // https://nextjs.org/docs/advanced-features/output-file-tracing 11 | output: "standalone", 12 | /** 13 | * If you have the "experimental: { appDir: true }" setting enabled, then you 14 | * must comment the below `i18n` config out. 15 | * 16 | * @see https://github.com/vercel/next.js/issues/41980 17 | */ 18 | i18n: { 19 | defaultLocale: "en", 20 | // localeDetection: false, 21 | locales: ["en", "fr", "no", "pl", "zh-tw", "zh", "es", "ru"], 22 | }, 23 | trailingSlash: true, 24 | eslint: { 25 | ignoreDuringBuilds: true, 26 | }, 27 | async redirects() { 28 | return [ 29 | { 30 | source: "/", 31 | destination: "/auth/login", 32 | permanent: true, 33 | }, 34 | ]; 35 | }, 36 | }; 37 | export default config; 38 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | 8 | module.exports = config; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20230714081023_flow/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `ipAssignments` to the `network` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "network" ADD COLUMN "autoAssignIp" BOOLEAN DEFAULT true, 9 | ADD COLUMN "flowRule" TEXT, 10 | ADD COLUMN "ipAssignments" TEXT NOT NULL; 11 | -------------------------------------------------------------------------------- /prisma/migrations/20230717095341_tags/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The `tags` column on the `network_members` table would be dropped and recreated. This will lead to data loss if there is data in the column. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "network" ADD COLUMN "capabilitiesByName" JSONB, 9 | ADD COLUMN "tagsByName" JSONB; 10 | 11 | -- AlterTable 12 | ALTER TABLE "network_members" ADD COLUMN "capabilities" JSONB, 13 | DROP COLUMN "tags", 14 | ADD COLUMN "tags" JSONB; 15 | -------------------------------------------------------------------------------- /prisma/migrations/20230717150852_nw_member/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[id,nwid]` on the table `network_members` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- DropIndex 8 | DROP INDEX "network_members_id_key"; 9 | 10 | -- CreateIndex 11 | CREATE UNIQUE INDEX "network_members_id_nwid_key" ON "network_members"("id", "nwid"); 12 | -------------------------------------------------------------------------------- /prisma/migrations/20230723084736_mailoptions/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "GlobalOptions" ADD COLUMN "forgotPasswordTemplate" JSONB, 3 | ADD COLUMN "inviteAdminTemplate" JSONB, 4 | ADD COLUMN "inviteUserTemplate" JSONB, 5 | ADD COLUMN "notificationTemplate" JSONB, 6 | ADD COLUMN "smtpEmail" TEXT, 7 | ADD COLUMN "smtpHost" TEXT, 8 | ADD COLUMN "smtpIgnoreTLS" BOOLEAN NOT NULL DEFAULT false, 9 | ADD COLUMN "smtpPassword" TEXT, 10 | ADD COLUMN "smtpPort" TEXT NOT NULL DEFAULT '587', 11 | ADD COLUMN "smtpRequireTLS" BOOLEAN NOT NULL DEFAULT false, 12 | ADD COLUMN "smtpSecure" BOOLEAN NOT NULL DEFAULT false, 13 | ADD COLUMN "smtpUseSSL" BOOLEAN NOT NULL DEFAULT false, 14 | ADD COLUMN "smtpUsername" TEXT, 15 | ADD COLUMN "userRegistrationNotification" BOOLEAN NOT NULL DEFAULT false, 16 | ADD COLUMN "verifyEmailTemplate" JSONB; 17 | -------------------------------------------------------------------------------- /prisma/migrations/20230724093114_nw_description/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "network" ADD COLUMN "creationTime" TIMESTAMP(3), 3 | ADD COLUMN "description" TEXT, 4 | ADD COLUMN "lastModifiedTime" TIMESTAMP(3); 5 | -------------------------------------------------------------------------------- /prisma/migrations/20230728082722_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "GlobalOptions" ADD COLUMN "showNotationMarkerInTableRow" BOOLEAN DEFAULT false, 3 | ADD COLUMN "useNotationColorAsBg" BOOLEAN DEFAULT false; 4 | 5 | -- CreateTable 6 | CREATE TABLE "Notation" ( 7 | "id" SERIAL NOT NULL, 8 | "name" TEXT NOT NULL, 9 | "color" TEXT, 10 | "description" TEXT, 11 | "creationTime" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 12 | "updatedTime" TIMESTAMP(3) NOT NULL, 13 | "isActive" BOOLEAN NOT NULL DEFAULT true, 14 | "nwid" TEXT NOT NULL, 15 | "icon" TEXT, 16 | "orderIndex" INTEGER, 17 | "visibility" TEXT, 18 | 19 | CONSTRAINT "Notation_pkey" PRIMARY KEY ("id") 20 | ); 21 | 22 | -- CreateTable 23 | CREATE TABLE "NetworkMemberNotation" ( 24 | "notationId" INTEGER NOT NULL, 25 | "nodeid" INTEGER NOT NULL, 26 | 27 | CONSTRAINT "NetworkMemberNotation_pkey" PRIMARY KEY ("notationId","nodeid") 28 | ); 29 | 30 | -- CreateIndex 31 | CREATE UNIQUE INDEX "Notation_name_nwid_key" ON "Notation"("name", "nwid"); 32 | 33 | -- AddForeignKey 34 | ALTER TABLE "Notation" ADD CONSTRAINT "Notation_nwid_fkey" FOREIGN KEY ("nwid") REFERENCES "network"("nwid") ON DELETE RESTRICT ON UPDATE CASCADE; 35 | 36 | -- AddForeignKey 37 | ALTER TABLE "NetworkMemberNotation" ADD CONSTRAINT "NetworkMemberNotation_notationId_fkey" FOREIGN KEY ("notationId") REFERENCES "Notation"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 38 | 39 | -- AddForeignKey 40 | ALTER TABLE "NetworkMemberNotation" ADD CONSTRAINT "NetworkMemberNotation_nodeid_fkey" FOREIGN KEY ("nodeid") REFERENCES "network_members"("nodeid") ON DELETE RESTRICT ON UPDATE CASCADE; 41 | -------------------------------------------------------------------------------- /prisma/migrations/20230806115334_zt_central/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `nwname` on the `network` table. All the data in the column will be lost. 5 | - You are about to drop the column `lastseen` on the `network_members` table. All the data in the column will be lost. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "GlobalOptions" ADD COLUMN "ztCentralApiKey" TEXT DEFAULT '', 10 | ADD COLUMN "ztCentralApiUrl" TEXT DEFAULT 'https://api.zerotier.com/api', 11 | ALTER COLUMN "showNotationMarkerInTableRow" SET DEFAULT true; 12 | 13 | -- AlterTable 14 | ALTER TABLE "network" DROP COLUMN "nwname", 15 | ADD COLUMN "name" TEXT; 16 | 17 | -- AlterTable 18 | ALTER TABLE "network_members" DROP COLUMN "lastseen", 19 | ADD COLUMN "lastSeen" TIMESTAMP(3); 20 | -------------------------------------------------------------------------------- /prisma/migrations/20230809202431_cascade/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "NetworkMemberNotation" DROP CONSTRAINT "NetworkMemberNotation_nodeid_fkey"; 3 | 4 | -- DropForeignKey 5 | ALTER TABLE "Notation" DROP CONSTRAINT "Notation_nwid_fkey"; 6 | 7 | -- DropForeignKey 8 | ALTER TABLE "network_members" DROP CONSTRAINT "network_members_nwid_fkey"; 9 | 10 | -- AddForeignKey 11 | ALTER TABLE "network_members" ADD CONSTRAINT "network_members_nwid_fkey" FOREIGN KEY ("nwid") REFERENCES "network"("nwid") ON DELETE CASCADE ON UPDATE CASCADE; 12 | 13 | -- AddForeignKey 14 | ALTER TABLE "Notation" ADD CONSTRAINT "Notation_nwid_fkey" FOREIGN KEY ("nwid") REFERENCES "network"("nwid") ON DELETE CASCADE ON UPDATE CASCADE; 15 | 16 | -- AddForeignKey 17 | ALTER TABLE "NetworkMemberNotation" ADD CONSTRAINT "NetworkMemberNotation_nodeid_fkey" FOREIGN KEY ("nodeid") REFERENCES "network_members"("nodeid") ON DELETE CASCADE ON UPDATE CASCADE; 18 | -------------------------------------------------------------------------------- /prisma/migrations/20230811063619_user_options/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `showNotationMarkerInTableRow` on the `GlobalOptions` table. All the data in the column will be lost. 5 | - You are about to drop the column `useNotationColorAsBg` on the `GlobalOptions` table. All the data in the column will be lost. 6 | - You are about to drop the column `ztCentralApiKey` on the `GlobalOptions` table. All the data in the column will be lost. 7 | - You are about to drop the column `ztCentralApiUrl` on the `GlobalOptions` table. All the data in the column will be lost. 8 | 9 | */ 10 | -- AlterTable 11 | ALTER TABLE "GlobalOptions" DROP COLUMN "showNotationMarkerInTableRow", 12 | DROP COLUMN "useNotationColorAsBg", 13 | DROP COLUMN "ztCentralApiKey", 14 | DROP COLUMN "ztCentralApiUrl"; 15 | 16 | -- CreateTable 17 | CREATE TABLE "UserOptions" ( 18 | "id" SERIAL NOT NULL, 19 | "userId" INTEGER NOT NULL, 20 | "useNotationColorAsBg" BOOLEAN DEFAULT false, 21 | "showNotationMarkerInTableRow" BOOLEAN DEFAULT true, 22 | "ztCentralApiKey" TEXT DEFAULT '', 23 | "ztCentralApiUrl" TEXT DEFAULT 'https://api.zerotier.com/api/v1', 24 | "localControllerUrl" TEXT DEFAULT 'http://zerotier:9993', 25 | "localControllerSecret" TEXT DEFAULT '', 26 | 27 | CONSTRAINT "UserOptions_pkey" PRIMARY KEY ("id") 28 | ); 29 | 30 | -- CreateIndex 31 | CREATE UNIQUE INDEX "UserOptions_userId_key" ON "UserOptions"("userId"); 32 | 33 | -- AddForeignKey 34 | ALTER TABLE "UserOptions" ADD CONSTRAINT "UserOptions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 35 | -------------------------------------------------------------------------------- /prisma/migrations/20230813195808_user_group/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "AccessLevel" AS ENUM ('READ_ONLY', 'WRITE', 'ADMINISTRATIVE'); 3 | 4 | -- AlterTable 5 | ALTER TABLE "User" ADD COLUMN "userGroupId" INTEGER; 6 | 7 | -- CreateTable 8 | CREATE TABLE "UserGroup" ( 9 | "id" SERIAL NOT NULL, 10 | "name" TEXT NOT NULL, 11 | "description" TEXT, 12 | "maxNetworks" INTEGER NOT NULL DEFAULT 5, 13 | "accessLevel" "AccessLevel" NOT NULL DEFAULT 'WRITE', 14 | "isDefault" BOOLEAN NOT NULL DEFAULT false, 15 | 16 | CONSTRAINT "UserGroup_pkey" PRIMARY KEY ("id") 17 | ); 18 | 19 | -- CreateIndex 20 | CREATE UNIQUE INDEX "UserGroup_name_key" ON "UserGroup"("name"); 21 | 22 | -- AddForeignKey 23 | ALTER TABLE "User" ADD CONSTRAINT "User_userGroupId_fkey" FOREIGN KEY ("userGroupId") REFERENCES "UserGroup"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 24 | -------------------------------------------------------------------------------- /prisma/migrations/20230815175550_custom_root/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "GlobalOptions" ADD COLUMN "customPlanetUsed" BOOLEAN NOT NULL DEFAULT false; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20230818113722_custom_root_extend/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "GlobalOptions" ADD COLUMN "plBirth" BIGINT NOT NULL DEFAULT 0, 3 | ADD COLUMN "plComment" TEXT, 4 | ADD COLUMN "plEndpoints" TEXT, 5 | ADD COLUMN "plID" BIGINT NOT NULL DEFAULT 0, 6 | ADD COLUMN "plIdentity" TEXT, 7 | ADD COLUMN "plRecommend" BOOLEAN NOT NULL DEFAULT false; 8 | -------------------------------------------------------------------------------- /prisma/migrations/20230823185550_update_cascade_delete/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "UserOptions" DROP CONSTRAINT "UserOptions_userId_fkey"; 3 | 4 | -- DropForeignKey 5 | ALTER TABLE "network" DROP CONSTRAINT "network_authorId_fkey"; 6 | 7 | -- AddForeignKey 8 | ALTER TABLE "network" ADD CONSTRAINT "network_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 9 | 10 | -- AddForeignKey 11 | ALTER TABLE "UserOptions" ADD CONSTRAINT "UserOptions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 12 | -------------------------------------------------------------------------------- /prisma/migrations/20230825053528_user_invitation/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "UserInvitation" ( 3 | "id" SERIAL NOT NULL, 4 | "token" TEXT NOT NULL, 5 | "used" BOOLEAN NOT NULL DEFAULT false, 6 | "email" TEXT, 7 | "secret" TEXT NOT NULL, 8 | "url" TEXT NOT NULL, 9 | "expires" TIMESTAMP(3) NOT NULL, 10 | "timesCanUse" INTEGER NOT NULL DEFAULT 1, 11 | "timesUsed" INTEGER NOT NULL DEFAULT 0, 12 | "createdBy" INTEGER NOT NULL, 13 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 14 | 15 | CONSTRAINT "UserInvitation_pkey" PRIMARY KEY ("id") 16 | ); 17 | 18 | -- CreateIndex 19 | CREATE UNIQUE INDEX "UserInvitation_token_key" ON "UserInvitation"("token"); 20 | -------------------------------------------------------------------------------- /prisma/migrations/20230910161252_authorize_warning/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "UserOptions" ADD COLUMN "deAuthorizeWarning" BOOLEAN DEFAULT false; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20231006203924_landing_page_body/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "GlobalOptions" ADD COLUMN "welcomeMessageBody" TEXT, 3 | ADD COLUMN "welcomeMessageEnabled" BOOLEAN NOT NULL DEFAULT false, 4 | ADD COLUMN "welcomeMessageTitle" TEXT; 5 | -------------------------------------------------------------------------------- /prisma/migrations/20231028155404_token_user_expiration/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "expiresAt" TIMESTAMP(3), 3 | ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true; 4 | 5 | -- CreateTable 6 | CREATE TABLE "APIToken" ( 7 | "id" SERIAL NOT NULL, 8 | "name" TEXT NOT NULL, 9 | "token" TEXT NOT NULL, 10 | "userId" INTEGER NOT NULL, 11 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 12 | "expiresAt" TIMESTAMP(3), 13 | "isActive" BOOLEAN NOT NULL DEFAULT true, 14 | 15 | CONSTRAINT "APIToken_pkey" PRIMARY KEY ("id") 16 | ); 17 | 18 | -- CreateIndex 19 | CREATE UNIQUE INDEX "APIToken_token_key" ON "APIToken"("token"); 20 | 21 | -- AddForeignKey 22 | ALTER TABLE "APIToken" ADD CONSTRAINT "APIToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 23 | -------------------------------------------------------------------------------- /prisma/migrations/20231204170707_cascade_delete_user/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "ActivityLog" DROP CONSTRAINT "ActivityLog_performedById_fkey"; 3 | 4 | -- DropForeignKey 5 | ALTER TABLE "LastReadMessage" DROP CONSTRAINT "LastReadMessage_lastMessageId_fkey"; 6 | 7 | -- DropForeignKey 8 | ALTER TABLE "LastReadMessage" DROP CONSTRAINT "LastReadMessage_userId_fkey"; 9 | 10 | -- DropForeignKey 11 | ALTER TABLE "MembershipRequest" DROP CONSTRAINT "MembershipRequest_userId_fkey"; 12 | 13 | -- DropForeignKey 14 | ALTER TABLE "Messages" DROP CONSTRAINT "Messages_userId_fkey"; 15 | 16 | -- DropForeignKey 17 | ALTER TABLE "UserOrganizationRole" DROP CONSTRAINT "UserOrganizationRole_userId_fkey"; 18 | 19 | -- AddForeignKey 20 | ALTER TABLE "UserOrganizationRole" ADD CONSTRAINT "UserOrganizationRole_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 21 | 22 | -- AddForeignKey 23 | ALTER TABLE "Messages" ADD CONSTRAINT "Messages_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 24 | 25 | -- AddForeignKey 26 | ALTER TABLE "LastReadMessage" ADD CONSTRAINT "LastReadMessage_lastMessageId_fkey" FOREIGN KEY ("lastMessageId") REFERENCES "Messages"("id") ON DELETE CASCADE ON UPDATE CASCADE; 27 | 28 | -- AddForeignKey 29 | ALTER TABLE "LastReadMessage" ADD CONSTRAINT "LastReadMessage_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 30 | 31 | -- AddForeignKey 32 | ALTER TABLE "MembershipRequest" ADD CONSTRAINT "MembershipRequest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 33 | 34 | -- AddForeignKey 35 | ALTER TABLE "ActivityLog" ADD CONSTRAINT "ActivityLog_performedById_fkey" FOREIGN KEY ("performedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 36 | -------------------------------------------------------------------------------- /prisma/migrations/20231221190212_member_id_as_name/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "UserOptions" ADD COLUMN "addMemberIdAsName" BOOLEAN DEFAULT false; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20231222102756_oauth/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[provider,providerAccountId]` on the table `Account` will be added. If there are existing duplicate values, this will fail. 5 | - Added the required column `provider` to the `Account` table without a default value. This is not possible if the table is not empty. 6 | - Added the required column `providerAccountId` to the `Account` table without a default value. This is not possible if the table is not empty. 7 | 8 | */ 9 | -- AlterTable 10 | ALTER TABLE "Account" ADD COLUMN "provider" TEXT NOT NULL, 11 | ADD COLUMN "providerAccountId" TEXT NOT NULL, 12 | ADD COLUMN "refresh_expires_in" INTEGER; 13 | 14 | -- AlterTable 15 | ALTER TABLE "User" ALTER COLUMN "hash" DROP NOT NULL; 16 | 17 | -- CreateIndex 18 | CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); 19 | -------------------------------------------------------------------------------- /prisma/migrations/20231229104733_webhooks/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Webhook" ( 3 | "id" TEXT NOT NULL, 4 | "name" TEXT NOT NULL, 5 | "description" TEXT NOT NULL, 6 | "url" TEXT NOT NULL, 7 | "enabled" BOOLEAN NOT NULL DEFAULT false, 8 | "eventTypes" JSONB NOT NULL, 9 | "secret" TEXT DEFAULT '', 10 | "lastDelivery" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, 11 | "organizationId" TEXT, 12 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 13 | "userId" TEXT, 14 | 15 | CONSTRAINT "Webhook_pkey" PRIMARY KEY ("id") 16 | ); 17 | 18 | -- AddForeignKey 19 | ALTER TABLE "Webhook" ADD CONSTRAINT "Webhook_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE SET NULL ON UPDATE CASCADE; 20 | 21 | -- AddForeignKey 22 | ALTER TABLE "Webhook" ADD CONSTRAINT "Webhook_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; 23 | -------------------------------------------------------------------------------- /prisma/migrations/20240310090305_physical_address/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "network_members" ADD COLUMN "physicalAddress" TEXT; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240315064831_api_token_extended/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The primary key for the `APIToken` table will be changed. If it partially fails, the table could be left without primary key constraint. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "APIToken" DROP CONSTRAINT "APIToken_pkey", 9 | ADD COLUMN "apiAuthorizationType" JSONB NOT NULL DEFAULT '["PERSONAL"]', 10 | ALTER COLUMN "id" DROP DEFAULT, 11 | ALTER COLUMN "id" SET DATA TYPE TEXT, 12 | ADD CONSTRAINT "APIToken_pkey" PRIMARY KEY ("id"); 13 | DROP SEQUENCE "APIToken_id_seq"; 14 | -------------------------------------------------------------------------------- /prisma/migrations/20240327130439_org_invites/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The primary key for the `OrganizationInvitation` table will be changed. If it partially fails, the table could be left without primary key constraint. 5 | - Added the required column `invitedById` to the `OrganizationInvitation` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "OrganizationInvitation" DROP CONSTRAINT "OrganizationInvitation_pkey", 10 | ADD COLUMN "expiresAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 11 | ADD COLUMN "invitedById" TEXT NOT NULL, 12 | ADD COLUMN "mailSentAt" TIMESTAMP(3), 13 | ADD COLUMN "role" "Role" NOT NULL DEFAULT 'READ_ONLY', 14 | ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 15 | ALTER COLUMN "id" DROP DEFAULT, 16 | ALTER COLUMN "id" SET DATA TYPE TEXT, 17 | ADD CONSTRAINT "OrganizationInvitation_pkey" PRIMARY KEY ("id"); 18 | DROP SEQUENCE "OrganizationInvitation_id_seq"; 19 | -------------------------------------------------------------------------------- /prisma/migrations/20240514144732_user_created_at/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `expirationDate` on the `User` table. All the data in the column will be lost. 5 | - You are about to drop the column `licenseKey` on the `User` table. All the data in the column will be lost. 6 | - You are about to drop the column `licenseStatus` on the `User` table. All the data in the column will be lost. 7 | - You are about to drop the column `orderId` on the `User` table. All the data in the column will be lost. 8 | - You are about to drop the column `orderStatus` on the `User` table. All the data in the column will be lost. 9 | - You are about to drop the column `product_id` on the `User` table. All the data in the column will be lost. 10 | 11 | */ 12 | -- AlterTable 13 | ALTER TABLE "User" DROP COLUMN "expirationDate", 14 | DROP COLUMN "licenseKey", 15 | DROP COLUMN "licenseStatus", 16 | DROP COLUMN "orderId", 17 | DROP COLUMN "orderStatus", 18 | DROP COLUMN "product_id", 19 | ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; 20 | 21 | -- AlterTable 22 | ALTER TABLE "UserInvitation" ADD COLUMN "groupId" TEXT; 23 | -------------------------------------------------------------------------------- /prisma/migrations/20240803114906_global_node_naming/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "OrganizationSettings" ADD COLUMN "renameNodeGlobally" BOOLEAN DEFAULT false; 3 | 4 | -- AlterTable 5 | ALTER TABLE "UserOptions" ADD COLUMN "renameNodeGlobally" BOOLEAN DEFAULT false; 6 | -------------------------------------------------------------------------------- /prisma/migrations/20240805143520_totp/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Invitation" ADD COLUMN "require2FA" BOOLEAN NOT NULL DEFAULT false; 3 | 4 | -- AlterTable 5 | ALTER TABLE "Organization" ADD COLUMN "require2FA" BOOLEAN NOT NULL DEFAULT false; 6 | 7 | -- AlterTable 8 | ALTER TABLE "User" ADD COLUMN "failedLoginAttempts" INTEGER NOT NULL DEFAULT 0, 9 | ADD COLUMN "lastFailedLoginAttempt" TIMESTAMP(3), 10 | ADD COLUMN "requestChangePassword" BOOLEAN NOT NULL DEFAULT false, 11 | ADD COLUMN "twoFactorEnabled" BOOLEAN NOT NULL DEFAULT false, 12 | ADD COLUMN "twoFactorRecoveryCodes" TEXT[] DEFAULT ARRAY[]::TEXT[], 13 | ADD COLUMN "twoFactorSecret" TEXT; 14 | -------------------------------------------------------------------------------- /prisma/migrations/20240809164834_oauth_provider/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Account" ADD COLUMN "expires_in" INTEGER, 3 | ADD COLUMN "ext_expires_in" INTEGER; 4 | -------------------------------------------------------------------------------- /prisma/migrations/20240826093637_user_device/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "UserDevice" ( 3 | "id" TEXT NOT NULL, 4 | "userId" TEXT NOT NULL, 5 | "deviceType" TEXT NOT NULL, 6 | "ipAddress" TEXT, 7 | "location" TEXT, 8 | "deviceId" TEXT NOT NULL, 9 | "browser" TEXT NOT NULL, 10 | "browserVersion" TEXT NOT NULL, 11 | "os" TEXT NOT NULL, 12 | "osVersion" TEXT NOT NULL, 13 | "lastActive" TIMESTAMP(3) NOT NULL, 14 | "isActive" BOOLEAN NOT NULL DEFAULT true, 15 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 16 | 17 | CONSTRAINT "UserDevice_pkey" PRIMARY KEY ("id") 18 | ); 19 | 20 | -- CreateIndex 21 | CREATE UNIQUE INDEX "UserDevice_deviceId_key" ON "UserDevice"("deviceId"); 22 | 23 | -- CreateIndex 24 | CREATE INDEX "UserDevice_userId_idx" ON "UserDevice"("userId"); 25 | 26 | -- CreateIndex 27 | CREATE INDEX "UserDevice_deviceId_idx" ON "UserDevice"("deviceId"); 28 | 29 | -- AddForeignKey 30 | ALTER TABLE "UserDevice" ADD CONSTRAINT "UserDevice_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 31 | -------------------------------------------------------------------------------- /prisma/migrations/20240827062445_device_notification/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `userAgent` to the `UserDevice` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "GlobalOptions" ADD COLUMN "deviceIpChangeNotificationTemplate" JSONB, 9 | ADD COLUMN "newDeviceNotificationTemplate" JSONB; 10 | 11 | -- AlterTable 12 | ALTER TABLE "UserDevice" ADD COLUMN "userAgent" TEXT NOT NULL; 13 | 14 | -- AlterTable 15 | ALTER TABLE "UserOptions" ADD COLUMN "apiRateLimitNotification" BOOLEAN DEFAULT true, 16 | ADD COLUMN "deviceIpChangeNotification" BOOLEAN DEFAULT true, 17 | ADD COLUMN "failedLoginNotification" BOOLEAN DEFAULT true, 18 | ADD COLUMN "newDeviceNotification" BOOLEAN DEFAULT true; 19 | -------------------------------------------------------------------------------- /prisma/migrations/20241215102802_sitename/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "GlobalOptions" ADD COLUMN "siteName" TEXT NOT NULL DEFAULT 'ZTNET'; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20241227084426_routes_table/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `routes` on the `network` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "network" DROP COLUMN "routes"; 9 | 10 | -- CreateTable 11 | CREATE TABLE "Routes" ( 12 | "id" TEXT NOT NULL, 13 | "target" TEXT NOT NULL, 14 | "via" TEXT, 15 | "networkId" TEXT NOT NULL, 16 | "notes" TEXT, 17 | 18 | CONSTRAINT "Routes_pkey" PRIMARY KEY ("id") 19 | ); 20 | 21 | -- AddForeignKey 22 | ALTER TABLE "Routes" ADD CONSTRAINT "Routes_networkId_fkey" FOREIGN KEY ("networkId") REFERENCES "network"("nwid") ON DELETE CASCADE ON UPDATE CASCADE; 23 | -------------------------------------------------------------------------------- /prisma/migrations/20250415055909_member_authorized/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "network_members" ADD COLUMN "authorized" BOOLEAN DEFAULT false; 3 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import { updateUserId } from "./seeds/update-user-id"; 2 | import { seedUserOptions } from "./seeds/user-option.seed"; 3 | import { PrismaClient } from "@prisma/client"; 4 | 5 | const prisma = new PrismaClient(); 6 | 7 | async function main() { 8 | await seedUserOptions(); 9 | await updateUserId(); 10 | } 11 | 12 | main() 13 | .catch((e) => { 14 | console.error(e); 15 | process.exit(1); 16 | }) 17 | .finally(async () => { 18 | await prisma.$disconnect(); 19 | }); 20 | -------------------------------------------------------------------------------- /prisma/seeds/user-option.seed.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | const prisma = new PrismaClient(); 4 | 5 | export async function seedUserOptions() { 6 | // Fetch all users from the database 7 | const users = await prisma.user.findMany(); 8 | 9 | for (const user of users) { 10 | // Check if UserOptions exist for each user 11 | const userOptionExists = await prisma.userOptions.findUnique({ 12 | where: { userId: user.id }, 13 | }); 14 | // If UserOptions do not exist for a user, create them 15 | if (!userOptionExists) { 16 | await prisma.userOptions.create({ 17 | data: { 18 | userId: user.id, 19 | useNotationColorAsBg: false, 20 | showNotationMarkerInTableRow: true, 21 | ztCentralApiKey: "", 22 | ztCentralApiUrl: "https://api.zerotier.com/api/v1", 23 | localControllerUrl: "http://zerotier:9993", 24 | localControllerSecret: "", 25 | }, 26 | }); 27 | } 28 | } 29 | // biome-ignore lint/suspicious/noConsoleLog: 30 | console.log("Seeding:: User Options complete!"); 31 | } 32 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/public/favicon.ico -------------------------------------------------------------------------------- /public/images/ztnet_200x178.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/public/images/ztnet_200x178.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ZTNET", 3 | "short_name": "ZTNET", 4 | "description": "ZeroTier Web UI for Private Controllers", 5 | "start_url": "/", 6 | "display": "standalone", 7 | "background_color": "#ffffff", 8 | "theme_color": "#000000", 9 | "icons": [ 10 | { 11 | "src": "/ztnet_300x300.png", 12 | "sizes": "300x300", 13 | "type": "image/png" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /public/service-worker.js: -------------------------------------------------------------------------------- 1 | const CACHE_NAME = 'my-nextjs-pwa-cache'; 2 | const urlsToCache = [ 3 | '/', 4 | '/manifest.json', 5 | '/icon.png' 6 | // Add other static assets to cache 7 | ]; 8 | self.addEventListener('install', (event) => { 9 | event.waitUntil( 10 | caches.open(CACHE_NAME).then((cache) => cache.addAll(urlsToCache)) 11 | ); 12 | }); 13 | self.addEventListener('fetch', (event) => { 14 | event.respondWith( 15 | caches.match(event.request).then((response) => { 16 | return response || fetch(event.request); 17 | }) 18 | ); 19 | }); -------------------------------------------------------------------------------- /public/ztnet_300x300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/public/ztnet_300x300.png -------------------------------------------------------------------------------- /src/__tests__/components/inputField.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, fireEvent, screen } from "@testing-library/react"; 3 | import InputField from "~/components/elements/inputField"; 4 | import { NextIntlClientProvider } from "next-intl"; 5 | import enTranslation from "~/locales/en/common.json"; 6 | 7 | describe("InputField", () => { 8 | // ...existing code... 9 | 10 | test("renderInputs: renders form with input elements and buttons", () => { 11 | const fields = [ 12 | { 13 | name: "testField", 14 | initialValue: "Initial value", 15 | type: "text", 16 | placeholder: "Test placeholder", 17 | }, 18 | ]; 19 | 20 | const submitHandler = jest.fn(); 21 | 22 | const { container } = render( 23 | 24 | 25 | , 26 | ); 27 | 28 | // Click the edit icon to render the inputs 29 | fireEvent.click(screen.getByTestId("view-form")); 30 | 31 | // Check if the form is rendered 32 | const formElement = container.querySelector("form"); 33 | expect(formElement).toBeInTheDocument(); 34 | 35 | // Check if the input elements are rendered 36 | const inputElement = screen.getByPlaceholderText("Test placeholder"); 37 | expect(inputElement).toBeInTheDocument(); 38 | 39 | // Check if the submit button is rendered 40 | const submitButton = screen.getByRole("button", { name: /submit/i }); 41 | expect(submitButton).toBeInTheDocument(); 42 | 43 | // Check if the cancel button is rendered 44 | const cancelButton = screen.getByRole("button", { name: /cancel/i }); 45 | expect(cancelButton).toBeInTheDocument(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/components/adminPage/controller/debugController.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import CodeMirror from "@uiw/react-codemirror"; 3 | import { okaidia } from "@uiw/codemirror-theme-okaidia"; 4 | import { python } from "@codemirror/lang-python"; 5 | 6 | interface Idata { 7 | data: unknown; 8 | title: string; 9 | isOpen?: () => void; 10 | } 11 | 12 | const DebugMirror = ({ data, title }: Idata) => { 13 | const [isOpen, setIsOpen] = useState(false); 14 | 15 | if (!data) return null; 16 | 17 | return ( 18 |
setIsOpen(!isOpen)} 20 | className="collapse-arrow collapse w-full border border-base-300 bg-base-200" 21 | > 22 | 23 |
{title}
24 |
25 | 37 |
38 |
39 | ); 40 | }; 41 | 42 | export default DebugMirror; 43 | -------------------------------------------------------------------------------- /src/components/adminPage/users/userGroup.tsx: -------------------------------------------------------------------------------- 1 | import { User } from "@prisma/client"; 2 | import React from "react"; 3 | import { 4 | useTrpcApiErrorHandler, 5 | useTrpcApiSuccessHandler, 6 | } from "~/hooks/useTrpcApiHandler"; 7 | import { api } from "~/utils/api"; 8 | 9 | interface Iuser { 10 | user: Partial; 11 | } 12 | const UserRole = ({ user }: Iuser) => { 13 | const handleApiError = useTrpcApiErrorHandler(); 14 | const handleApiSuccess = useTrpcApiSuccessHandler(); 15 | 16 | // const t = useTranslations("admin"); 17 | const { data: usergroups } = api.admin.getUserGroups.useQuery(); 18 | // will update the users table as it uses key "getUsers" 19 | // !TODO should rework to update local cache instead.. but this works for now 20 | const { refetch: refetchUsers } = api.admin.getUsers.useQuery({ 21 | isAdmin: false, 22 | }); 23 | 24 | // Updates this modal as it uses key "getUser" 25 | // !TODO should rework to update local cache instead.. but this works for now 26 | const { refetch: refetchUser } = api.admin.getUser.useQuery({ 27 | userId: user?.id, 28 | }); 29 | 30 | const { mutate: assignUserGroup } = api.admin.assignUserGroup.useMutation({ 31 | onError: handleApiError, 32 | onSuccess: handleApiSuccess({ 33 | actions: [refetchUser, refetchUsers], 34 | }), 35 | }); 36 | 37 | return ( 38 |
39 | 58 |
59 | ); 60 | }; 61 | 62 | export default UserRole; 63 | -------------------------------------------------------------------------------- /src/components/adminPage/users/userRole.tsx: -------------------------------------------------------------------------------- 1 | import { User } from "@prisma/client"; 2 | import { useTranslations } from "next-intl"; 3 | import React from "react"; 4 | import { 5 | useTrpcApiErrorHandler, 6 | useTrpcApiSuccessHandler, 7 | } from "~/hooks/useTrpcApiHandler"; 8 | import { api } from "~/utils/api"; 9 | 10 | interface Iuser { 11 | user: Partial; 12 | } 13 | const UserRole = ({ user }: Iuser) => { 14 | const t = useTranslations("admin"); 15 | 16 | const handleApiError = useTrpcApiErrorHandler(); 17 | const handleApiSuccess = useTrpcApiSuccessHandler(); 18 | 19 | // will update the users table as it uses key "getUsers" 20 | // !TODO should rework to update local cache instead.. but this works for now 21 | const { refetch: refetchUsers } = api.admin.getUsers.useQuery({ 22 | isAdmin: false, 23 | }); 24 | 25 | // Updates this modal as it uses key "getUser" 26 | // !TODO should rework to update local cache instead.. but this works for now 27 | const { refetch: refetchUser } = api.admin.getUser.useQuery({ 28 | userId: user?.id, 29 | }); 30 | 31 | const { mutate: changeRole } = api.admin.changeRole.useMutation({ 32 | onSuccess: handleApiSuccess({ 33 | actions: [refetchUsers, refetchUser], 34 | toastMessage: t("users.users.toastMessages.roleChangeSuccess"), 35 | }), 36 | onError: handleApiError, 37 | }); 38 | const dropDownHandler = (e: React.ChangeEvent, id: string) => { 39 | changeRole({ 40 | id, 41 | role: e.target.value, 42 | }); 43 | }; 44 | 45 | return ( 46 |
47 | 55 |
56 | ); 57 | }; 58 | 59 | export default UserRole; 60 | -------------------------------------------------------------------------------- /src/components/auth/formInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from "react"; 2 | 3 | interface FormInputProps { 4 | label: string; 5 | name: string; 6 | type: string; 7 | value: string; 8 | onChange: (event: React.ChangeEvent) => void; 9 | placeholder: string; 10 | disabled?: boolean; 11 | icon?: ReactElement; 12 | } 13 | 14 | const FormInput: React.FC = ({ 15 | label, 16 | name, 17 | type, 18 | value, 19 | onChange, 20 | placeholder, 21 | disabled = false, 22 | icon, 23 | }) => { 24 | return ( 25 |
26 | 29 | 42 |
43 | ); 44 | }; 45 | 46 | export default FormInput; 47 | -------------------------------------------------------------------------------- /src/components/auth/formSubmitButton.tsx: -------------------------------------------------------------------------------- 1 | import cn from "classnames"; 2 | 3 | interface SubmitButtonProps { 4 | loading: boolean; 5 | title: string; 6 | } 7 | 8 | const FormSubmitButtons: React.FC = ({ loading, title }) => ( 9 | 18 | ); 19 | 20 | export default FormSubmitButtons; 21 | -------------------------------------------------------------------------------- /src/components/auth/multifactorNotEnabledAlert.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslations } from "next-intl"; 2 | import React from "react"; 3 | 4 | interface MultifactorNotEnabledProps { 5 | className?: string; 6 | } 7 | 8 | const MultifactorNotEnabled: React.FC = ({ 9 | className = "", 10 | }) => { 11 | const t = useTranslations("userSettings"); 12 | 13 | return ( 14 |
15 | 29 |
30 |

{t("account.totp.notification.title")}

31 |
{t("account.totp.notification.description")}
32 |
33 |
34 | ); 35 | }; 36 | 37 | export default MultifactorNotEnabled; 38 | -------------------------------------------------------------------------------- /src/components/auth/totpInput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import TwoFactAuth from "./totpDigits"; 3 | import Link from "next/link"; 4 | import { useTranslations } from "next-intl"; 5 | 6 | interface TOTPInputProps { 7 | totpCode: string; 8 | setTotpCode: (code: string) => void; 9 | } 10 | 11 | const TOTPInput: React.FC = ({ totpCode, setTotpCode }) => { 12 | const t = useTranslations(); 13 | return ( 14 |
15 | 18 | setTotpCode(val)} /> 19 | 23 | {t("authPages.signin.havingIssues")} 24 | 25 |
26 | ); 27 | }; 28 | 29 | export default TOTPInput; 30 | -------------------------------------------------------------------------------- /src/components/auth/welcomeMessage.tsx: -------------------------------------------------------------------------------- 1 | import { api } from "~/utils/api"; 2 | import Link from "next/link"; 3 | import { useTranslations } from "next-intl"; 4 | 5 | export const WelcomeMessage = () => { 6 | const t = useTranslations(); 7 | const { data: options, isLoading } = api.public.getWelcomeMessage.useQuery(); 8 | 9 | return ( 10 |
11 |
12 | {!isLoading && ( 13 |
14 |
15 | {options?.welcomeMessageTitle ? ( 16 |

{options.welcomeMessageTitle}

17 | ) : ( 18 |
19 |

20 |
21 | ztnet logo 27 | ZTNET 28 |
29 |

30 | 36 | https://ztnet.network 37 | 38 |
39 | )} 40 |
41 |

42 | {options?.welcomeMessageBody || ( 43 | {t("authPages.welcomeMessage.slogan")} 44 | )} 45 |

46 |
47 | )} 48 |
49 |
50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/components/auth/withAuth.tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps } from "next"; 2 | import { getSession } from "next-auth/react"; 3 | 4 | export function withAuth(gssp: GetServerSideProps): GetServerSideProps { 5 | return async (context) => { 6 | const { user } = (await getSession(context)) || {}; 7 | 8 | if (!user) { 9 | return { 10 | redirect: { statusCode: 302, destination: "/auth/login" }, 11 | }; 12 | } 13 | // ssp (server side props) 14 | const gsspData = await gssp(context); 15 | 16 | if (!("props" in gsspData)) { 17 | throw new Error("invalid getSSP result"); 18 | } 19 | 20 | return { 21 | props: { 22 | ...gsspData.props, 23 | user, 24 | }, 25 | }; 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/components/elements/debouncedInput.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, type InputHTMLAttributes } from "react"; 2 | 3 | // A debounced input react component 4 | export const DebouncedInput = ({ 5 | value: initialValue, 6 | onChange, 7 | debounce = 500, 8 | ...props 9 | }: { 10 | value: string | number; 11 | onChange: (value: string | number) => void; 12 | debounce?: number; 13 | } & Omit, "onChange">) => { 14 | const [value, setValue] = useState(initialValue); 15 | 16 | useEffect(() => { 17 | setValue(initialValue); 18 | }, [initialValue]); 19 | 20 | // biome-ignore lint/correctness/useExhaustiveDependencies: 21 | useEffect(() => { 22 | const timeout = setTimeout(() => { 23 | onChange(value); 24 | }, debounce); 25 | 26 | return () => clearTimeout(timeout); 27 | }, [value]); 28 | 29 | return ( 30 | setValue(e.target.value)} 35 | /> 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/layouts/footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { globalSiteVersion } from "~/utils/global"; 3 | 4 | const Footer = () => ( 5 |
6 |
7 |
8 |

9 | © Kodea Solutions {new Date().getFullYear()}. All rights reserved 10 |

11 | {globalSiteVersion ? ( 12 |

{globalSiteVersion}

13 | ) : ( 14 |
15 | )} 16 |
17 |
18 | ); 19 | 20 | export default Footer; 21 | -------------------------------------------------------------------------------- /src/components/networkByIdPage/flowRule/flagsAndTags.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useFlagsAndTags } from "./useFlagsAndTags"; 3 | import TagComponent from "./tagComponent"; 4 | 5 | interface IProp { 6 | organizationId: string; 7 | nwid: string; 8 | memberId: string; 9 | central?: boolean; 10 | } 11 | 12 | const FlagsAndTags = ({ organizationId, nwid, memberId, central = false }: IProp) => { 13 | const { tagsByName, tagFlags, handleEnumChange, handleFlagsCheckboxChange } = 14 | useFlagsAndTags({ 15 | organizationId, 16 | nwid, 17 | memberId, 18 | central, 19 | }); 20 | 21 | if (!tagsByName || Object.keys(tagsByName).length === 0) { 22 | return

None

; 23 | } 24 | 25 | return ( 26 |
27 | {Object.entries(tagsByName).map(([tagName, tagDetails]) => ( 28 | 36 | ))} 37 |
38 | ); 39 | }; 40 | 41 | export default FlagsAndTags; 42 | -------------------------------------------------------------------------------- /src/components/networkByIdPage/networkQrCode.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from "next-themes"; 2 | import { QRCodeSVG } from "qrcode.react"; 3 | import React, { useEffect, useState } from "react"; 4 | import daisyuiColors from "daisyui/src/theming/themes"; 5 | import Link from "next/link"; 6 | 7 | interface IProps { 8 | networkId: string; 9 | } 10 | 11 | const urlBuilder = (networkId: string) => { 12 | return `https://joinzt.com/addnetwork?nwid=${networkId}&v=1`; 13 | }; 14 | 15 | const NetworkQrCode = ({ networkId }: IProps) => { 16 | const [themeRGBColor, setThemeRGBColor] = useState(""); 17 | const { theme } = useTheme(); 18 | 19 | useEffect(() => { 20 | setThemeRGBColor(daisyuiColors[theme]?.primary); 21 | }, [theme]); 22 | 23 | return ( 24 | 25 | 33 | 34 | ); 35 | }; 36 | 37 | export default NetworkQrCode; 38 | -------------------------------------------------------------------------------- /src/components/networkByIdPage/networkRoutes/collumns.tsx: -------------------------------------------------------------------------------- 1 | import { createColumnHelper } from "@tanstack/react-table"; 2 | import { MemberEntity } from "~/types/local/member"; 3 | import { RoutesEntity } from "~/types/local/network"; 4 | 5 | const columnHelper = createColumnHelper(); 6 | 7 | export const networkRoutesColumns = ( 8 | deleteRoute: (route: RoutesEntity) => void, 9 | isUpdating: boolean, 10 | members: MemberEntity[], 11 | ) => [ 12 | columnHelper.accessor("id", { 13 | cell: (info) => info.getValue(), 14 | }), 15 | columnHelper.accessor("target", { 16 | header: "Destination", 17 | cell: (info) => info.getValue(), 18 | }), 19 | columnHelper.accessor("via", { 20 | header: "Via", 21 | cell: (info) =>
{info.row.original.via || "LAN"}
, 22 | }), 23 | columnHelper.accessor("nodeName", { 24 | header: "Node Name", 25 | cell: (info) => { 26 | // check if ipAssignments has the via ip and return the node name 27 | const node = members?.find((member) => 28 | member.ipAssignments?.includes(info.row.original.via), 29 | ); 30 | return node?.name || null; 31 | }, 32 | }), 33 | columnHelper.accessor("notes", { 34 | header: "Notes", 35 | }), 36 | columnHelper.accessor("actions", { 37 | header: "", 38 | cell: (info) => ( 39 | !isUpdating && deleteRoute(info.row.original)} 47 | > 48 | 53 | 54 | ), 55 | }), 56 | ]; 57 | -------------------------------------------------------------------------------- /src/components/networkByIdPage/privatePublic.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface CardProps { 4 | title: string; 5 | content: string; 6 | rootClassName: string; 7 | iconClassName: string; 8 | faded: boolean; 9 | onClick: () => void; 10 | } 11 | 12 | const CardComponent: React.FC = ({ 13 | onClick, 14 | title, 15 | content, 16 | rootClassName, 17 | iconClassName, 18 | faded, 19 | }) => { 20 | return ( 21 |
25 |
26 |
27 | {title} 28 | 29 | {/* */} 30 | 38 | 43 | 44 | 45 |
46 |
47 | {content} 48 |
49 |
50 |
51 | ); 52 | }; 53 | 54 | export default CardComponent; 55 | -------------------------------------------------------------------------------- /src/components/networkPage/networkTableMemberCount.tsx: -------------------------------------------------------------------------------- 1 | export const NetworkTableMemberCount = ({ count }) => { 2 | return ( 3 |
4 | {count} 5 |
6 | ); 7 | }; 8 | -------------------------------------------------------------------------------- /src/components/organization/orgUserRole.tsx: -------------------------------------------------------------------------------- 1 | import { User } from "@prisma/client"; 2 | import { useTranslations } from "next-intl"; 3 | import React from "react"; 4 | import { 5 | useTrpcApiErrorHandler, 6 | useTrpcApiSuccessHandler, 7 | } from "~/hooks/useTrpcApiHandler"; 8 | import { api } from "~/utils/api"; 9 | 10 | interface Iuser { 11 | user: Partial; 12 | organizationId: string; 13 | } 14 | const OrgUserRole = ({ user, organizationId }: Iuser) => { 15 | const t = useTranslations("admin"); 16 | 17 | const handleApiError = useTrpcApiErrorHandler(); 18 | const handleApiSuccess = useTrpcApiSuccessHandler(); 19 | 20 | const { refetch: refecthOrg } = api.org.getOrgUsers.useQuery({ 21 | organizationId, 22 | }); 23 | 24 | const { data: orgUserRole, refetch: refetchUserRole } = 25 | api.org.getOrgUserRoleById.useQuery({ 26 | organizationId, 27 | userId: user?.id, 28 | }); 29 | 30 | const { mutate: changeRole } = api.org.changeUserRole.useMutation({ 31 | onSuccess: handleApiSuccess({ 32 | actions: [refecthOrg, refetchUserRole], 33 | toastMessage: t("users.users.toastMessages.roleChangeSuccess"), 34 | }), 35 | onError: handleApiError, 36 | }); 37 | 38 | const dropDownHandler = (e: React.ChangeEvent, userId: string) => { 39 | changeRole({ 40 | userId, 41 | role: e.target.value, 42 | organizationId, 43 | }); 44 | }; 45 | 46 | return ( 47 |
48 | 57 |
58 | ); 59 | }; 60 | 61 | export default OrgUserRole; 62 | -------------------------------------------------------------------------------- /src/components/shared/menuSectionDividerWrapper.tsx: -------------------------------------------------------------------------------- 1 | import type React from "react"; 2 | 3 | interface MenuSectionWrapperProps { 4 | title: string; 5 | children: React.ReactNode; 6 | className?: string; 7 | } 8 | 9 | const MenuSectionDividerWrapper: React.FC = ({ 10 | title, 11 | children, 12 | className = "", 13 | }) => { 14 | return ( 15 |
16 |
17 |

18 | {title} 19 |

20 |
21 |
22 | {children} 23 |
24 | ); 25 | }; 26 | 27 | export default MenuSectionDividerWrapper; 28 | -------------------------------------------------------------------------------- /src/components/shared/metaTags.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import React from "react"; 3 | 4 | const HeadSection = ({ title }: { title: string }) => { 5 | return ( 6 | 7 | {title} 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default HeadSection; 16 | -------------------------------------------------------------------------------- /src/components/shared/networkLoadingSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import cn from "classnames"; 3 | 4 | interface IProps { 5 | className?: string; 6 | } 7 | 8 | const NetworkLoadingSkeleton = ({ className }: IProps) => { 9 | return ( 10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | ); 36 | }; 37 | 38 | export default NetworkLoadingSkeleton; 39 | -------------------------------------------------------------------------------- /src/components/userSettings/fontSize.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useTranslations } from "next-intl"; 3 | import { useFontSizeStore } from "~/utils/store"; 4 | 5 | const fontSizeOptions = { 6 | Small: "text-xs", 7 | Medium: "text-base", 8 | Large: "text-lg", 9 | }; 10 | 11 | const ApplicationFontSize = () => { 12 | const t = useTranslations("userSettings"); 13 | const { fontSize, setFontSize } = useFontSizeStore(); 14 | const [localFontSize, setLocalFontSize] = useState(fontSize); 15 | 16 | useEffect(() => { 17 | // Apply the font size class to the document element 18 | document.documentElement.className = fontSizeOptions[localFontSize]; 19 | // Save the font size to the store whenever it changes 20 | setFontSize(localFontSize); 21 | }, [localFontSize, setFontSize]); 22 | 23 | return ( 24 |
25 |
26 | 31 | 42 |
43 |
44 | ); 45 | }; 46 | 47 | export default ApplicationFontSize; 48 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | import "@tanstack/react-table"; 2 | /* eslint-disable @typescript-eslint/consistent-type-imports */ 3 | /* eslint-disable @typescript-eslint/no-empty-interface */ 4 | // Use type safe message keys with `next-intl` 5 | type Messages = typeof import("./locales/en/common.json"); 6 | declare type IntlMessages = Messages; 7 | 8 | declare module "@tanstack/table-core" { 9 | interface ColumnMeta { 10 | style: { 11 | textAlign: "left" | "center" | "right"; 12 | }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/hooks/useDynamicViewportHeight.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useCallback, useRef } from "react"; 2 | 3 | // biome-ignore lint/suspicious/noExplicitAny: 4 | const useDynamicViewportHeight = (dependencies: any[] = []) => { 5 | const headerRef = useRef(null); 6 | 7 | const updateViewportHeight = useCallback(() => { 8 | if (!headerRef.current) return; 9 | const headerHeight = headerRef.current.offsetHeight; 10 | const vh = window.innerHeight * 0.01; 11 | document.documentElement.style.setProperty("--vh", `${vh}px`); 12 | document.documentElement.style.setProperty("--header-height", `${headerHeight}px`); 13 | }, []); 14 | 15 | useEffect(() => { 16 | updateViewportHeight(); 17 | window.addEventListener("resize", updateViewportHeight); 18 | return () => window.removeEventListener("resize", updateViewportHeight); 19 | }, [updateViewportHeight, ...dependencies]); 20 | 21 | return headerRef; 22 | }; 23 | 24 | export default useDynamicViewportHeight; 25 | -------------------------------------------------------------------------------- /src/hooks/useHandleResize.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useSidebarStore } from "~/utils/store"; 3 | 4 | // Create a custom hook 5 | export const useHandleResize = () => { 6 | const { setOpenState } = useSidebarStore(); 7 | 8 | // biome-ignore lint/correctness/useExhaustiveDependencies: 9 | useEffect(() => { 10 | const handleResize = () => { 11 | if (window.innerWidth >= 768) return setOpenState(true); 12 | 13 | setOpenState(false); 14 | }; 15 | 16 | // Initial check 17 | handleResize(); 18 | 19 | window.addEventListener("resize", handleResize); 20 | 21 | return () => { 22 | window.removeEventListener("resize", handleResize); 23 | }; 24 | }, []); 25 | }; 26 | -------------------------------------------------------------------------------- /src/hooks/useOrganizationWebsocket.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useSocketStore } from "~/utils/store"; 3 | 4 | type OrganizationId = { 5 | id: string; 6 | }; 7 | 8 | const useOrganizationWebsocket = (orgIds: OrganizationId[]) => { 9 | const setupSocket = useSocketStore((state) => state.setupSocket); 10 | const cleanupSocket = useSocketStore((state) => state.cleanupSocket); 11 | 12 | useEffect(() => { 13 | // Call setupSocket when the component mounts or orgIds change 14 | if (orgIds && orgIds.length > 0) { 15 | setupSocket(orgIds); 16 | } 17 | 18 | // Return a function that calls cleanupSocket when the component unmounts 19 | return () => { 20 | cleanupSocket(); 21 | }; 22 | }, [orgIds, setupSocket, cleanupSocket]); // Dependencies array 23 | }; 24 | 25 | export default useOrganizationWebsocket; 26 | -------------------------------------------------------------------------------- /src/hooks/useSkipper.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from "react"; 2 | 3 | export const useSkipper = () => { 4 | const shouldSkipRef = useRef(true); 5 | const shouldSkip = shouldSkipRef.current; 6 | 7 | // Wrap a function with this to skip a pagination reset temporarily 8 | const skip = useCallback(() => { 9 | shouldSkipRef.current = false; 10 | }, []); 11 | 12 | useEffect(() => { 13 | shouldSkipRef.current = true; 14 | }); 15 | 16 | return [shouldSkip, skip] as const; 17 | }; 18 | -------------------------------------------------------------------------------- /src/hooks/useTrpcApiHandler.tsx: -------------------------------------------------------------------------------- 1 | import { TRPCClientErrorLike } from "@trpc/client"; 2 | import { useTranslations } from "next-intl"; 3 | 4 | import toast from "react-hot-toast"; 5 | import { ErrorData } from "~/types/errorHandling"; 6 | 7 | // biome-ignore lint/suspicious/noExplicitAny: 8 | type TRPCErrorLike = TRPCClientErrorLike & { data?: ErrorData }; 9 | 10 | export const useTrpcApiErrorHandler = () => { 11 | const t = useTranslations("commonToast"); 12 | 13 | const handleError = (error: TRPCErrorLike) => { 14 | if ((error.data as ErrorData)?.zodError) { 15 | const fieldErrors = (error.data as ErrorData)?.zodError.fieldErrors; 16 | for (const field in fieldErrors) { 17 | toast.error(`${fieldErrors[field].join(", ")}`); 18 | } 19 | } else if (error.message) { 20 | toast.error(error.message); 21 | } else { 22 | toast.error(t("errorOccurred")); 23 | } 24 | }; 25 | 26 | return handleError; 27 | }; 28 | 29 | interface SuccessHandlerOptions { 30 | actions?: (() => void)[]; 31 | toastMessage?: string; 32 | } 33 | 34 | export const useTrpcApiSuccessHandler = () => { 35 | const t = useTranslations("commonToast"); 36 | 37 | const handleSuccess = ({ actions = [], toastMessage }: SuccessHandlerOptions) => { 38 | return () => { 39 | // Display the custom toast message if provided, otherwise use the default 40 | toast.success(toastMessage || t("updatedSuccessfully")); 41 | 42 | // Refetch all provided queries using for...of loop 43 | for (const action of actions) { 44 | void action(); 45 | } 46 | }; 47 | }; 48 | 49 | return handleSuccess; 50 | }; 51 | -------------------------------------------------------------------------------- /src/icons/IOSIcon.tsx: -------------------------------------------------------------------------------- 1 | interface Icon { 2 | className?: string; 3 | onClick?: () => void; 4 | fill?: string; 5 | } 6 | 7 | export const IOSIcon = ({ className = "", onClick, ...rest }: Icon) => ( 8 | 9 | 18 | 23 | 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /src/icons/androidIcon.tsx: -------------------------------------------------------------------------------- 1 | interface Icon { 2 | className?: string; 3 | onClick?: () => void; 4 | fill?: string; 5 | } 6 | 7 | export const AndroidIcon = ({ className = "", onClick, ...rest }: Icon) => ( 8 | 9 | 18 | 23 | 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /src/icons/copy.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface Icon { 4 | // add optional className prop 5 | className?: string; 6 | onClick?: () => void; 7 | } 8 | 9 | const CopyIcon = ({ className = "", onClick, ...rest }: Icon) => ( 10 | 11 | 20 | 25 | 26 | 27 | ); 28 | 29 | export default CopyIcon; 30 | -------------------------------------------------------------------------------- /src/icons/edit.tsx: -------------------------------------------------------------------------------- 1 | interface Icon { 2 | // add optional className prop 3 | className?: string; 4 | onClick?: () => void; 5 | } 6 | 7 | const EditIcon = ({ className, onClick, ...rest }: Icon) => { 8 | return ( 9 | 10 | 19 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default EditIcon; 30 | -------------------------------------------------------------------------------- /src/icons/monitor.tsx: -------------------------------------------------------------------------------- 1 | interface Icon { 2 | // add optional className prop 3 | className?: string; 4 | onClick?: () => void; 5 | } 6 | 7 | const Monitor = ({ className, onClick, ...rest }: Icon) => { 8 | return ( 9 | 10 | 19 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default Monitor; 30 | -------------------------------------------------------------------------------- /src/icons/plusIcon.tsx: -------------------------------------------------------------------------------- 1 | // icon:plus | Ant Design Icons https://ant.design/components/icon/ | Ant Design 2 | import * as React from "react"; 3 | 4 | function IconPlus(props) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | } 15 | 16 | export default IconPlus; 17 | -------------------------------------------------------------------------------- /src/icons/smartphone.tsx: -------------------------------------------------------------------------------- 1 | interface Icon { 2 | // add optional className prop 3 | className?: string; 4 | onClick?: () => void; 5 | } 6 | 7 | const SmartPhone = ({ className, onClick, ...rest }: Icon) => { 8 | return ( 9 | 10 | 19 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default SmartPhone; 30 | -------------------------------------------------------------------------------- /src/icons/tablet.tsx: -------------------------------------------------------------------------------- 1 | interface Icon { 2 | // add optional className prop 3 | className?: string; 4 | onClick?: () => void; 5 | } 6 | 7 | const Tablet = ({ className, onClick, ...rest }: Icon) => { 8 | return ( 9 | 10 | 19 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default Tablet; 30 | -------------------------------------------------------------------------------- /src/icons/verified.tsx: -------------------------------------------------------------------------------- 1 | interface Icon { 2 | className?: string; 3 | onClick?: () => void; 4 | } 5 | 6 | const Verified = ({ className, onClick, ...rest }: Icon) => { 7 | return ( 8 | 9 | 16 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default Verified; 27 | -------------------------------------------------------------------------------- /src/icons/windowsIcon.tsx: -------------------------------------------------------------------------------- 1 | interface Icon { 2 | className?: string; 3 | onClick?: () => void; 4 | fill?: string; 5 | } 6 | 7 | export const WindowsIcon = ({ className = "", onClick, ...rest }: Icon) => ( 8 | 9 | 18 | 23 | 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /src/instrumentation.ts: -------------------------------------------------------------------------------- 1 | export async function register() { 2 | if (process.env.NEXT_RUNTIME === "nodejs") { 3 | const cronTasksModule = await import("./cronTasks"); 4 | if (cronTasksModule.CheckExpiredUsers) { 5 | cronTasksModule.CheckExpiredUsers(); 6 | } 7 | 8 | // update lastseen for all members 9 | if (cronTasksModule.updatePeers) { 10 | cronTasksModule.updatePeers(); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/locales/lang.ts: -------------------------------------------------------------------------------- 1 | export const supportedLocales = ["en", "fr", "no", "zh", "pl", "zh-tw", "es", "ru"]; 2 | export const languageNames = { 3 | default: "System", 4 | en: "English", 5 | fr: "French", 6 | no: "Norwegian", 7 | pl: "Polish", 8 | zh: "Chinese", 9 | "zh-tw": "Traditional Chinese", 10 | es: "Spanish", 11 | ru: "Русский", 12 | }; 13 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/pages/admin/organization/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactElement } from "react"; 2 | import { LayoutAuthenticated } from "~/components/layouts/layout"; 3 | import { useTranslations } from "next-intl"; 4 | import AddOrgForm from "~/components/networkByIdPage/organization/addOrgForm"; 5 | import ListOrganizations from "~/components/networkByIdPage/organization/listOrganizations"; 6 | import MenuSectionDividerWrapper from "~/components/shared/menuSectionDividerWrapper"; 7 | 8 | const Organization = () => { 9 | const t = useTranslations("admin"); 10 | 11 | return ( 12 |
13 | 17 |
18 |

19 | {t("organization.addOrganization.description")} 20 |

21 |
22 |
23 | 24 |
25 | 26 |
27 |
28 | ); 29 | }; 30 | 31 | Organization.getLayout = function getLayout(page: ReactElement) { 32 | return {page}; 33 | }; 34 | export default Organization; 35 | -------------------------------------------------------------------------------- /src/pages/api/__tests__/v1/application/statistic.test.ts: -------------------------------------------------------------------------------- 1 | import apiStatsHandler from "~/pages/api/v1/stats/index"; 2 | import { NextApiRequest, NextApiResponse } from "next"; 3 | 4 | describe("/api/stats", () => { 5 | it("should allow only GET method", async () => { 6 | const methods = ["DELETE", "POST", "PUT", "PATCH", "OPTIONS", "HEAD"]; 7 | const req = { 8 | method: "GET", 9 | headers: { 10 | "x-ztnet-auth": "validApiKey", 11 | }, 12 | query: {}, 13 | body: {}, 14 | } as unknown as NextApiRequest; 15 | 16 | const res = { 17 | status: jest.fn().mockReturnThis(), 18 | end: jest.fn(), 19 | json: jest.fn().mockReturnThis(), 20 | setHeader: jest.fn(), 21 | } as unknown as NextApiResponse; 22 | 23 | for (const method of methods) { 24 | req.method = method; 25 | await apiStatsHandler(req, res); 26 | 27 | expect(res.status).toHaveBeenCalledWith(405); 28 | 29 | // expect json to be called with text "Method Not Allowed" 30 | expect(res.json).toHaveBeenCalledWith( 31 | expect.objectContaining({ error: "Method Not Allowed" }), 32 | ); 33 | } 34 | }); 35 | 36 | it("should respond 401 when invalid API key for GET", async () => { 37 | const req = { 38 | method: "GET", 39 | headers: { "x-ztnet-auth": "invalidApiKey" }, 40 | query: {}, 41 | body: {}, 42 | } as unknown as NextApiRequest; 43 | 44 | const res = { 45 | status: jest.fn().mockReturnThis(), 46 | end: jest.fn(), 47 | json: jest.fn().mockReturnThis(), 48 | setHeader: jest.fn(), // Mock `setHeader` rate limiter uses it 49 | } as unknown as NextApiResponse; 50 | 51 | await apiStatsHandler(req, res); 52 | 53 | expect(res.status).toHaveBeenCalledWith(401); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/pages/api/__tests__/v1/network/network.test.ts: -------------------------------------------------------------------------------- 1 | import apiNetworkHandler from "~/pages/api/v1/network"; 2 | import { NextApiRequest, NextApiResponse } from "next"; 3 | 4 | describe("/api/createNetwork", () => { 5 | it("should respond 405 to unsupported methods", async () => { 6 | const req = { method: "PUT", query: {} } as NextApiRequest; 7 | const res = { 8 | status: jest.fn().mockReturnThis(), 9 | end: jest.fn(), 10 | json: jest.fn().mockReturnThis(), 11 | setHeader: jest.fn(), 12 | } as unknown as NextApiResponse; 13 | 14 | await apiNetworkHandler(req, res); 15 | 16 | expect(res.status).toHaveBeenCalledWith(405); 17 | }); 18 | 19 | it("should respond 401 when invalid API key for POST", async () => { 20 | const req = { 21 | method: "POST", 22 | headers: { "x-ztnet-auth": "invalidApiKey" }, 23 | query: {}, 24 | } as unknown as NextApiRequest; 25 | const res = { 26 | status: jest.fn().mockReturnThis(), 27 | end: jest.fn(), 28 | json: jest.fn().mockReturnThis(), 29 | setHeader: jest.fn(), 30 | } as unknown as NextApiResponse; 31 | 32 | await apiNetworkHandler(req, res); 33 | 34 | expect(res.status).toHaveBeenCalledWith(401); 35 | }); 36 | 37 | it("should respond 401 when invalid API key for GET", async () => { 38 | const req = { 39 | method: "GET", 40 | headers: { "x-ztnet-auth": "invalidApiKey" }, 41 | query: {}, 42 | } as unknown as NextApiRequest; 43 | const res = { 44 | status: jest.fn().mockReturnThis(), 45 | end: jest.fn(), 46 | json: jest.fn().mockReturnThis(), 47 | setHeader: jest.fn(), 48 | } as unknown as NextApiResponse; 49 | 50 | await apiNetworkHandler(req, res); 51 | 52 | expect(res.status).toHaveBeenCalledWith(401); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import NextAuth from "next-auth"; 3 | import { getAuthOptions } from "~/server/auth"; 4 | 5 | export default async function auth(req: NextApiRequest, res: NextApiResponse) { 6 | const authOptions = getAuthOptions(req, res); 7 | 8 | return await NextAuth(req, res, authOptions); 9 | } 10 | -------------------------------------------------------------------------------- /src/pages/api/auth/user/invalidateUser.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import { getServerSession } from "next-auth/next"; 3 | import { NextResponse } from "next/server"; 4 | import { getAuthOptions } from "~/server/auth"; 5 | import { prisma } from "~/server/db"; 6 | 7 | export default async function handler( 8 | request: NextApiRequest, 9 | response: NextApiResponse, 10 | ) { 11 | if (request.method !== "POST") { 12 | return response.status(405).json({ message: "Method not allowed" }); 13 | } 14 | 15 | try { 16 | const session = await getServerSession( 17 | request, 18 | response, 19 | getAuthOptions(request, response), 20 | ); 21 | if (!session) { 22 | return response.status(401).json({ message: "Not authenticated" }); 23 | } 24 | const { userId, deviceId } = await request.body; 25 | 26 | if (!userId || !deviceId) { 27 | return NextResponse.json({ error: "Missing userId or deviceId" }, { status: 400 }); 28 | } 29 | 30 | const userDevice = await prisma.userDevice.findUnique({ 31 | where: { 32 | userId, 33 | deviceId, 34 | }, 35 | }); 36 | 37 | if (!userDevice) { 38 | // Device doesn't exist, invalidate the token 39 | await prisma.user.update({ 40 | where: { id: userId }, 41 | data: { isActive: false }, 42 | }); 43 | 44 | return NextResponse.json({ message: "User invalidated" }, { status: 200 }); 45 | } 46 | 47 | // Update lastActive field 48 | await prisma.userDevice.update({ 49 | where: { 50 | userId, 51 | deviceId, 52 | }, 53 | data: { 54 | lastActive: new Date(), 55 | }, 56 | }); 57 | 58 | return NextResponse.json({ message: "Device updated" }, { status: 200 }); 59 | } catch (error) { 60 | console.error("Error in user invalidation:", error); 61 | return NextResponse.json({ error: "Internal server error" }, { status: 500 }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/pages/api/auth/user/invalidateUserDevice.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import { deleteDeviceCookie } from "~/utils/devices"; 3 | 4 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 5 | if (req.method === "POST") { 6 | // Set the cookie with an expiration date in the past 7 | deleteDeviceCookie(res); 8 | 9 | res.status(200).json({ message: "Device cookie deleted" }); 10 | } else { 11 | res.status(405).end(`Method ${req.method} Not Allowed`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/api/planet.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import fs from "fs"; 3 | import path from "path"; 4 | import { ZT_FOLDER } from "~/utils/ztApi"; 5 | 6 | export const config = { 7 | api: { 8 | bodyParser: false, 9 | }, 10 | }; 11 | 12 | export default async (req: NextApiRequest, res: NextApiResponse) => { 13 | // download planet file. 14 | // The planet.custom is signed by the server, so it can be trusted. 15 | // No authentication is required. 16 | 17 | if (req.method === "GET") { 18 | try { 19 | const folderPath = path.resolve(`${ZT_FOLDER}/zt-mkworld`); 20 | const filePath = path.join(folderPath, "planet.custom"); 21 | 22 | // Check if the directory and file exist 23 | if ( 24 | !fs.existsSync(folderPath) || 25 | !fs.statSync(folderPath).isDirectory() || 26 | !fs.existsSync(filePath) 27 | ) { 28 | return res.status(404).send("Folder or file not found."); 29 | } 30 | 31 | // Read the file and stream it to the response 32 | const fileStream = fs.createReadStream(filePath); 33 | 34 | // Set the headers 35 | res.setHeader("Content-Disposition", "attachment; filename=planet.custom"); 36 | res.setHeader("Content-Type", "application/octet-stream"); 37 | 38 | // Pipe the read stream to the response 39 | fileStream.pipe(res); 40 | } catch (error) { 41 | console.error(error); 42 | res.status(500).send("Internal Server Error."); 43 | } 44 | } else { 45 | res.status(405).send("Method Not Allowed"); // Handle unsupported HTTP methods 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /src/pages/api/trpc/[trpc].ts: -------------------------------------------------------------------------------- 1 | import { createNextApiHandler } from "@trpc/server/adapters/next"; 2 | 3 | import { env } from "~/env.mjs"; 4 | import { createTRPCContext } from "~/server/api/trpc"; 5 | import { appRouter } from "~/server/api/root"; 6 | 7 | // export API handler 8 | export default createNextApiHandler({ 9 | router: appRouter, 10 | createContext: createTRPCContext, 11 | onError: 12 | env.NODE_ENV === "development" 13 | ? ({ path, error }) => { 14 | console.error(`❌ tRPC failed on ${path ?? ""}: ${error.message}`); 15 | } 16 | : undefined, 17 | }); 18 | -------------------------------------------------------------------------------- /src/pages/api/v1/network/[id]/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import { prisma } from "~/server/db"; 3 | import { SecuredPrivateApiRoute } from "~/utils/apiRouteAuth"; 4 | import { handleApiErrors } from "~/utils/errors"; 5 | import rateLimit from "~/utils/rateLimit"; 6 | import * as ztController from "~/utils/ztApi"; 7 | 8 | // Number of allowed requests per minute 9 | const limiter = rateLimit({ 10 | interval: 60 * 1000, // 60 seconds 11 | uniqueTokenPerInterval: 500, // Max 500 users per second 12 | }); 13 | 14 | const REQUEST_PR_MINUTE = 50; 15 | 16 | export default async function apiNetworkByIdHandler( 17 | req: NextApiRequest, 18 | res: NextApiResponse, 19 | ) { 20 | try { 21 | await limiter.check(res, REQUEST_PR_MINUTE, "NETWORKBYID_CACHE_TOKEN"); // 10 requests per minute 22 | } catch { 23 | return res.status(429).json({ error: "Rate limit exceeded" }); 24 | } 25 | 26 | // create a switch based on the HTTP method 27 | switch (req.method) { 28 | case "GET": 29 | await GET_network(req, res); 30 | break; 31 | default: 32 | res.status(405).json({ error: "Method Not Allowed" }); 33 | break; 34 | } 35 | } 36 | 37 | const GET_network = SecuredPrivateApiRoute( 38 | { 39 | requireNetworkId: true, 40 | }, 41 | async (_req, res, { networkId, ctx, userId }) => { 42 | // get the network details 43 | const network = await prisma.network.findUnique({ 44 | where: { nwid: networkId, authorId: userId }, 45 | select: { authorId: true, description: true }, 46 | }); 47 | 48 | try { 49 | const ztControllerResponse = await ztController.local_network_detail( 50 | //@ts-expect-error 51 | ctx, 52 | networkId, 53 | false, 54 | ); 55 | return res.status(200).json({ 56 | ...network, 57 | ...ztControllerResponse?.network, 58 | }); 59 | } catch (cause) { 60 | return handleApiErrors(cause, res); 61 | } 62 | }, 63 | ); 64 | -------------------------------------------------------------------------------- /src/pages/api/v1/network/[id]/member/[memberId]/_schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | // Schema for updateable fields metadata 4 | export const updateableFieldsMetaSchema = z 5 | .object({ 6 | name: z.string().optional(), 7 | authorized: z.boolean().optional(), 8 | }) 9 | .strict(); 10 | 11 | // Schema for the context passed to the handler 12 | export const handlerContextSchema = z.object({ 13 | body: z.record(z.unknown()), 14 | userId: z.string(), 15 | networkId: z.string(), 16 | memberId: z.string(), 17 | ctx: z.object({ 18 | prisma: z.any(), 19 | session: z.object({ 20 | user: z.object({ 21 | id: z.string(), 22 | }), 23 | }), 24 | }), 25 | }); 26 | 27 | // Schema for the context passed to the DELETE handler 28 | export const deleteHandlerContextSchema = z.object({ 29 | userId: z.string(), 30 | networkId: z.string(), 31 | memberId: z.string(), 32 | ctx: z.object({ 33 | prisma: z.any(), 34 | session: z.object({ 35 | user: z.object({ 36 | id: z.string(), 37 | }), 38 | }), 39 | }), 40 | }); 41 | -------------------------------------------------------------------------------- /src/pages/api/v1/network/[id]/member/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import { prisma } from "~/server/db"; 3 | import { SecuredPrivateApiRoute } from "~/utils/apiRouteAuth"; 4 | import { handleApiErrors } from "~/utils/errors"; 5 | import rateLimit from "~/utils/rateLimit"; 6 | import * as ztController from "~/utils/ztApi"; 7 | 8 | // Number of allowed requests per minute 9 | const limiter = rateLimit({ 10 | interval: 60 * 1000, // 60 seconds 11 | uniqueTokenPerInterval: 500, // Max 500 users per second 12 | }); 13 | 14 | const REQUEST_PR_MINUTE = 50; 15 | 16 | export default async function apiNetworkMembersHandler( 17 | req: NextApiRequest, 18 | res: NextApiResponse, 19 | ) { 20 | try { 21 | await limiter.check(res, REQUEST_PR_MINUTE, "NETWORK_MEMBERS_CACHE_TOKEN"); // 10 requests per minute 22 | } catch { 23 | return res.status(429).json({ error: "Rate limit exceeded" }); 24 | } 25 | 26 | // create a switch based on the HTTP method 27 | switch (req.method) { 28 | case "GET": 29 | await GET_networkMembers(req, res); 30 | break; 31 | default: // Method Not Allowed 32 | res.status(405).json({ error: "Method Not Allowed" }); 33 | break; 34 | } 35 | } 36 | 37 | const GET_networkMembers = SecuredPrivateApiRoute( 38 | { 39 | requireNetworkId: true, 40 | }, 41 | async (_req, res, { networkId, ctx }) => { 42 | try { 43 | const controllerMember = await ztController.local_network_detail( 44 | // @ts-expect-error: fake request object 45 | ctx, 46 | networkId, 47 | false, 48 | ); 49 | 50 | const networkMembers = await Promise.all( 51 | controllerMember.members.map(async (member) => { 52 | const dbMember = await prisma.network_members.findUnique({ 53 | where: { 54 | id_nwid: { 55 | nwid: member.nwid, 56 | id: member.id, 57 | }, 58 | }, 59 | }); 60 | 61 | return { ...dbMember, ...member }; 62 | }), 63 | ); 64 | 65 | return res.status(200).json(networkMembers); 66 | } catch (cause) { 67 | return handleApiErrors(cause, res); 68 | } 69 | }, 70 | ); 71 | -------------------------------------------------------------------------------- /src/pages/api/v1/network/_schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | // Schema for the request body when creating a new network 4 | export const createNetworkBodySchema = z 5 | .object({ 6 | name: z.string().optional(), 7 | }) 8 | .strict(); 9 | 10 | // Schema for the context passed to the handler 11 | export const createNetworkContextSchema = z.object({ 12 | body: createNetworkBodySchema, 13 | ctx: z.object({ 14 | prisma: z.any(), 15 | session: z.object({ 16 | user: z.object({ 17 | id: z.string(), 18 | }), 19 | }), 20 | }), 21 | }); 22 | -------------------------------------------------------------------------------- /src/pages/api/v1/org/[orgid]/index.ts: -------------------------------------------------------------------------------- 1 | import { Role } from "@prisma/client"; 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | import { appRouter } from "~/server/api/root"; 4 | import { SecuredOrganizationApiRoute } from "~/utils/apiRouteAuth"; 5 | import { handleApiErrors } from "~/utils/errors"; 6 | import rateLimit from "~/utils/rateLimit"; 7 | 8 | // Number of allowed requests per minute 9 | const limiter = rateLimit({ 10 | interval: 60 * 1000, // 60 seconds 11 | uniqueTokenPerInterval: 500, // Max 500 users per second 12 | }); 13 | 14 | export const REQUEST_PR_MINUTE = 50; 15 | 16 | export default async function apiNetworkHandler( 17 | req: NextApiRequest, 18 | res: NextApiResponse, 19 | ) { 20 | try { 21 | await limiter.check(res, REQUEST_PR_MINUTE, "ORGANIZATION_GET_CACHE_TOKEN"); 22 | } catch { 23 | return res.status(429).json({ error: "Rate limit exceeded" }); 24 | } 25 | 26 | // create a switch based on the HTTP method 27 | switch (req.method) { 28 | case "GET": 29 | await GET_orgById(req, res); 30 | break; 31 | default: // Method Not Allowed 32 | res.status(405).json({ error: "Method Not Allowed" }); 33 | break; 34 | } 35 | } 36 | 37 | export const GET_orgById = SecuredOrganizationApiRoute( 38 | { requiredRole: Role.READ_ONLY }, 39 | async (_req, res, { orgId, ctx }) => { 40 | try { 41 | //@ts-expect-error 42 | const caller = appRouter.createCaller(ctx); 43 | const organization = await caller.org 44 | .getOrgById({ 45 | organizationId: orgId, 46 | }) 47 | // modify the response to only inlude certain fields 48 | .then((org) => { 49 | return { 50 | id: org.id, 51 | name: org.orgName, 52 | createdAt: org.createdAt, 53 | ownerId: org.ownerId, 54 | networks: org.networks.map((network) => { 55 | return { 56 | nwid: network.nwid, 57 | name: network.name, 58 | }; 59 | }), 60 | }; 61 | }); 62 | 63 | return res.status(200).json(organization); 64 | } catch (cause) { 65 | return handleApiErrors(cause, res); 66 | } 67 | }, 68 | ); 69 | -------------------------------------------------------------------------------- /src/pages/api/v1/org/[orgid]/network/[nwid]/_schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | // Schema for updateable fields 4 | export const NetworkUpdateSchema = z 5 | .object({ 6 | name: z.string().optional(), 7 | description: z.string().optional(), 8 | flowRule: z.string().optional(), 9 | mtu: z.string().optional(), 10 | private: z.boolean().optional(), 11 | dns: z 12 | .object({ 13 | domain: z.string(), 14 | servers: z.array(z.string()), 15 | }) 16 | .optional(), 17 | ipAssignmentPools: z.array(z.unknown()).optional(), 18 | routes: z.array(z.unknown()).optional(), 19 | v4AssignMode: z.record(z.unknown()).optional(), 20 | v6AssignMode: z.record(z.unknown()).optional(), 21 | }) 22 | .strict(); 23 | 24 | // Schema for POST request body 25 | const PostBodySchema = z.record(z.unknown()); 26 | 27 | // Schema for the context passed to the handler 28 | export const HandlerContextSchema = z.object({ 29 | networkId: z.string(), 30 | ctx: z.object({ 31 | prisma: z.any(), 32 | session: z.object({ 33 | user: z.object({ 34 | id: z.string(), 35 | }), 36 | }), 37 | }), 38 | body: PostBodySchema, 39 | }); 40 | -------------------------------------------------------------------------------- /src/pages/api/v1/org/[orgid]/network/[nwid]/member/[memberId]/_schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | // Schema for POST request body 4 | export const PostBodySchema = z 5 | .object({ 6 | name: z.string().optional(), 7 | authorized: z.boolean().optional(), 8 | }) 9 | .strict(); 10 | 11 | // Schema for the context passed to the handler 12 | export const HandlerContextSchema = z.object({ 13 | networkId: z.string(), 14 | orgId: z.string(), 15 | memberId: z.string(), 16 | userId: z.string(), 17 | body: z.record(z.unknown()), 18 | ctx: z.object({ 19 | prisma: z.any(), 20 | session: z.object({ 21 | user: z.object({ 22 | id: z.string(), 23 | }), 24 | }), 25 | }), 26 | }); 27 | -------------------------------------------------------------------------------- /src/pages/api/v1/org/[orgid]/network/_schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | // Schema for the request body when creating a new network 4 | export const createNetworkBodySchema = z 5 | .object({ 6 | name: z.string().optional(), 7 | }) 8 | .strict(); 9 | 10 | // Schema for the context passed to the handler 11 | export const createNetworkContextSchema = z.object({ 12 | body: createNetworkBodySchema, 13 | orgId: z.string(), 14 | ctx: z.object({ 15 | prisma: z.any(), 16 | session: z.object({ 17 | user: z.object({ 18 | id: z.string(), 19 | }), 20 | }), 21 | }), 22 | }); 23 | -------------------------------------------------------------------------------- /src/pages/api/v1/org/[orgid]/user/index.ts: -------------------------------------------------------------------------------- 1 | import { Role } from "@prisma/client"; 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | import { appRouter } from "~/server/api/root"; 4 | import { SecuredOrganizationApiRoute } from "~/utils/apiRouteAuth"; 5 | import { handleApiErrors } from "~/utils/errors"; 6 | import rateLimit from "~/utils/rateLimit"; 7 | 8 | // Number of allowed requests per minute 9 | const limiter = rateLimit({ 10 | interval: 60 * 1000, // 60 seconds 11 | uniqueTokenPerInterval: 500, // Max 500 users per second 12 | }); 13 | 14 | const REQUEST_PR_MINUTE = 50; 15 | 16 | export default async function apiNetworkHandler( 17 | req: NextApiRequest, 18 | res: NextApiResponse, 19 | ) { 20 | try { 21 | await limiter.check(res, REQUEST_PR_MINUTE, "ORGANIZATION_GET_USER_CACHE_TOKEN"); // 10 requests per minute 22 | } catch { 23 | return res.status(429).json({ error: "Rate limit exceeded" }); 24 | } 25 | 26 | // create a switch based on the HTTP method 27 | switch (req.method) { 28 | case "GET": 29 | await GET_organizationUsers(req, res); 30 | break; 31 | default: // Method Not Allowed 32 | res.status(405).json({ error: "Method Not Allowed" }); 33 | break; 34 | } 35 | } 36 | 37 | const GET_organizationUsers = SecuredOrganizationApiRoute( 38 | { requiredRole: Role.USER }, 39 | async (_req, res, { orgId, ctx }) => { 40 | try { 41 | // @ts-expect-error ctx is not a valid parameter 42 | const caller = appRouter.createCaller(ctx); 43 | const orgUsers = await caller.org 44 | .getOrgUsers({ 45 | organizationId: orgId, 46 | }) 47 | .then((users) => { 48 | return users.map((user) => ({ 49 | orgId: orgId, 50 | userId: user.id, 51 | name: user.name, 52 | email: user.email, 53 | role: user.role, 54 | })); 55 | }); 56 | 57 | return res.status(200).json(orgUsers); 58 | } catch (cause) { 59 | return handleApiErrors(cause, res); 60 | } 61 | }, 62 | ); 63 | -------------------------------------------------------------------------------- /src/pages/api/v1/user/_schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { passwordSchema } from "~/server/api/routers/_schema"; 3 | 4 | // Input validation schema 5 | export const createUserSchema = z.object({ 6 | email: z 7 | .string() 8 | .email() 9 | .transform((val) => val.trim()), 10 | password: passwordSchema("password does not meet the requirements!"), 11 | name: z.string().min(3, "Name must contain at least 3 character(s)").max(40), 12 | expiresAt: z.string().datetime().optional(), 13 | generateApiToken: z.boolean().optional(), 14 | }); 15 | -------------------------------------------------------------------------------- /src/pages/api/websocket/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import { getServerSession } from "next-auth/next"; 3 | import { Server } from "socket.io"; 4 | import { getAuthOptions } from "~/server/auth"; 5 | 6 | interface SocketIoExtension { 7 | socket: { 8 | server: { 9 | io: Server; 10 | }; 11 | }; 12 | } 13 | 14 | export type NextApiResponseWithSocketIo = NextApiResponse & SocketIoExtension; 15 | const SocketHandler = async (req: NextApiRequest, res: NextApiResponseWithSocketIo) => { 16 | const session = await getServerSession(req, res, getAuthOptions(req, res)); 17 | if (!session) { 18 | res.status(401).json({ message: "Authorization Error" }); 19 | return; 20 | } 21 | 22 | if (!res.socket.server.io) { 23 | // biome-ignore lint/suspicious/noConsoleLog: 24 | console.log("Socket is initializing"); 25 | 26 | //@ts-expect-error assinging to a property that doesn't exist 27 | const io = new Server(res.socket.server, { 28 | addTrailingSlash: false, 29 | }); 30 | res.socket.server.io = io; 31 | 32 | // io.on("connection", (socket) => { 33 | // socket.on("join", ({ roomId }) => { 34 | // socket.join(roomId); 35 | // }); 36 | // }); 37 | } 38 | res.end(); 39 | }; 40 | 41 | export const config = { 42 | api: { 43 | bodyParser: false, 44 | }, 45 | }; 46 | 47 | export default SocketHandler; 48 | -------------------------------------------------------------------------------- /src/server/api/root.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCRouter } from "~/server/api/trpc"; 2 | import { authRouter } from "./routers/authRouter"; 3 | import { networkMemberRouter } from "./routers/memberRouter"; 4 | import { networkRouter } from "./routers/networkRouter"; 5 | import { adminRouter } from "./routers/adminRoute"; 6 | import { settingsRouter } from "./routers/settingsRouter"; 7 | import { organizationRouter } from "./routers/organizationRouter"; 8 | import { publicRouter } from "./routers/publicRouter"; 9 | import { mfaAuthRouter } from "./routers/mfaAuthRouter"; 10 | 11 | /** 12 | * This is the primary router for your server. 13 | * 14 | * All routers added in /api/routers should be manually added here. 15 | */ 16 | export const appRouter = createTRPCRouter({ 17 | network: networkRouter, 18 | networkMember: networkMemberRouter, 19 | auth: authRouter, 20 | mfaAuth: mfaAuthRouter, 21 | admin: adminRouter, 22 | settings: settingsRouter, 23 | org: organizationRouter, 24 | public: publicRouter, 25 | }); 26 | 27 | // export type definition of API 28 | export type AppRouter = typeof appRouter; 29 | -------------------------------------------------------------------------------- /src/server/api/routers/_schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | // This regular expression (regex) is used to validate a password based on the following criteria: 4 | // - The password must be at least 6 characters long. 5 | // - The password must contain at least two of the following three character types: 6 | // - Lowercase letters (a-z) 7 | // - Uppercase letters (A-Z) 8 | // - Digits (0-9) 9 | export const mediumPassword = 10 | /^(((?=.*[a-z])(?=.*[A-Z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[A-Z])(?=.*[0-9])))(?=.{6,})/; 11 | 12 | // create a zod password schema 13 | export const passwordSchema = (errorMessage: string) => 14 | z 15 | .string() 16 | .max(40, { message: "Password must not exceed 40 characters" }) 17 | .refine((val) => mediumPassword.test(val), { 18 | message: errorMessage, 19 | }) 20 | .optional(); 21 | -------------------------------------------------------------------------------- /src/server/api/routers/publicRouter.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; 2 | 3 | export const publicRouter = createTRPCRouter({ 4 | registrationAllowed: publicProcedure.query(async ({ ctx }) => { 5 | return await ctx.prisma.globalOptions.findFirst({ 6 | where: { 7 | id: 1, 8 | }, 9 | select: { 10 | enableRegistration: true, 11 | }, 12 | }); 13 | }), 14 | getWelcomeMessage: publicProcedure.query(async ({ ctx }) => { 15 | return await ctx.prisma.globalOptions.findFirst({ 16 | where: { 17 | id: 1, 18 | }, 19 | select: { 20 | welcomeMessageTitle: true, 21 | welcomeMessageBody: true, 22 | }, 23 | }); 24 | }), 25 | }); 26 | -------------------------------------------------------------------------------- /src/server/api/routers/settingsRouter.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/api/trpc"; 2 | import { SMTP_SECRET, decrypt, generateInstanceSecret } from "~/utils/encryption"; 3 | 4 | export const settingsRouter = createTRPCRouter({ 5 | // Set global options 6 | getAllOptions: protectedProcedure.query(async ({ ctx }) => { 7 | const options = await ctx.prisma.globalOptions.findFirst({ 8 | where: { 9 | id: 1, 10 | }, 11 | }); 12 | if (options?.smtpPassword) { 13 | try { 14 | options.smtpPassword = decrypt( 15 | options.smtpPassword, 16 | generateInstanceSecret(SMTP_SECRET), 17 | ); 18 | } catch (_err) { 19 | console.warn( 20 | "Failed to decrypt SMTP password. Has the NextAuth secret been changed?. Re-save the SMTP password to fix this.", 21 | ); 22 | } 23 | } 24 | return options; 25 | }), 26 | getPublicOptions: publicProcedure.query(async ({ ctx }) => { 27 | const publicOptions = await ctx.prisma.globalOptions.findFirst({ 28 | where: { 29 | id: 1, 30 | }, 31 | select: { 32 | siteName: true, 33 | }, 34 | }); 35 | 36 | return publicOptions; 37 | }), 38 | }); 39 | -------------------------------------------------------------------------------- /src/server/api/utils/memberUtils.ts: -------------------------------------------------------------------------------- 1 | import { MemberEntity } from "~/types/local/member"; 2 | import { isPrivateIP } from "./ipUtils"; 3 | 4 | export enum ConnectionStatus { 5 | Offline = 0, 6 | Relayed = 1, 7 | DirectLAN = 2, 8 | DirectWAN = 3, 9 | Controller = 4, 10 | } 11 | 12 | export function determineConnectionStatus(member: MemberEntity): ConnectionStatus { 13 | const regex = new RegExp(`^${member.id}`); 14 | if (regex.test(member.nwid)) { 15 | return ConnectionStatus.Controller; 16 | } 17 | // fix for zt version 1.12. Return type of peer is object!. 18 | if (!member?.peers || Object.keys(member?.peers).length === 0) { 19 | return ConnectionStatus.Offline; 20 | } 21 | 22 | if (Array.isArray(member?.peers) && member?.peers.length === 0) { 23 | return ConnectionStatus.Offline; 24 | } 25 | 26 | if (member?.peers?.latency === -1 || member?.peers?.versionMajor === -1) { 27 | return ConnectionStatus.Relayed; 28 | } 29 | 30 | // Check if at least one path has a private IP 31 | if (member?.peers?.paths && member?.peers.paths.length > 0) { 32 | for (const path of member.peers.paths) { 33 | const ip = path.address.split("/")[0]; 34 | if (isPrivateIP(ip)) { 35 | return ConnectionStatus.DirectLAN; 36 | } 37 | } 38 | } 39 | 40 | return ConnectionStatus.DirectWAN; 41 | } 42 | -------------------------------------------------------------------------------- /src/server/callbacks/session.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage } from "http"; 2 | import { prisma } from "../db"; 3 | 4 | export function sessionCallback( 5 | _req: IncomingMessage & { cookies: Partial<{ [key: string]: string }> }, 6 | ) { 7 | return async function authSession({ session, token }) { 8 | if (!token.id) return null; 9 | if (!token.deviceId) { 10 | return { ...session, user: null }; 11 | } 12 | // Check the user exists in the database 13 | const user = await prisma.user.findFirst({ 14 | where: { id: token.id }, 15 | }); 16 | // Number(user.id.trim()) checks if the user session has the old int as the User id 17 | if (!user || !user.isActive || Number.isInteger(Number(token.id))) { 18 | // If the user does not exist, set user to null 19 | return { ...session, user: null }; 20 | } 21 | 22 | // update users lastseen in the database 23 | await prisma.user.update({ 24 | where: { 25 | id: user.id, 26 | }, 27 | data: { 28 | lastseen: new Date(), 29 | }, 30 | }); 31 | 32 | return { 33 | ...session, 34 | user: { 35 | ...token, 36 | }, 37 | }; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/server/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | import { env } from "~/env.mjs"; 4 | 5 | const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }; 6 | 7 | export const prisma = 8 | globalForPrisma.prisma || 9 | new PrismaClient({ 10 | log: 11 | // removed query as it spams the console 12 | // env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], 13 | env.NODE_ENV === "development" ? ["error", "warn"] : ["error"], 14 | }); 15 | 16 | if (env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; 17 | -------------------------------------------------------------------------------- /src/server/getServerSideProps.ts: -------------------------------------------------------------------------------- 1 | import { getSession } from "next-auth/react"; 2 | import { withAuth } from "~/components/auth/withAuth"; 3 | import { prisma } from "./db"; 4 | import { GetServerSidePropsContext } from "next"; 5 | 6 | export const getServerSideProps = withAuth(async (context: GetServerSidePropsContext) => { 7 | const session = await getSession(context); 8 | const orgIds = await prisma.organization.findMany({ 9 | where: { 10 | users: { 11 | some: { 12 | id: session.user.id, 13 | }, 14 | }, 15 | }, 16 | select: { 17 | id: true, 18 | }, 19 | }); 20 | return { 21 | props: { 22 | orgIds, 23 | session, 24 | messages: (await import(`~/locales/${context.locale}/common.json`)).default, 25 | }, 26 | }; 27 | }); 28 | -------------------------------------------------------------------------------- /src/server/helpers/errorHandler.ts: -------------------------------------------------------------------------------- 1 | import { TRPCError } from "@trpc/server"; 2 | import { type AxiosError } from "axios"; 3 | 4 | export class APIError extends Error { 5 | statusText: string; 6 | status: number; 7 | cause?: Error; 8 | constructor(message?: string, axiosError?: AxiosError) { 9 | super(message || "An unknown error occurred"); 10 | 11 | if (axiosError) { 12 | this.name = "APIError"; 13 | this.status = axiosError.response?.status; 14 | this.statusText = axiosError.response?.statusText; 15 | this.cause = axiosError.cause; 16 | } 17 | } 18 | } 19 | 20 | export type ErrorCode = 21 | | "BAD_REQUEST" 22 | | "PARSE_ERROR" 23 | | "INTERNAL_SERVER_ERROR" 24 | | "UNAUTHORIZED" 25 | | "FORBIDDEN" 26 | | "NOT_FOUND" 27 | | "METHOD_NOT_SUPPORTED" 28 | | "TIMEOUT" 29 | | "CONFLICT" 30 | | "PRECONDITION_FAILED" 31 | | "PAYLOAD_TOO_LARGE" 32 | | "UNPROCESSABLE_CONTENT" 33 | | "TOO_MANY_REQUESTS" 34 | | "CLIENT_CLOSED_REQUEST"; 35 | 36 | export const throwError = ( 37 | message: string, 38 | code: ErrorCode = "BAD_REQUEST", 39 | cause: Error | null = null, 40 | ) => { 41 | throw new TRPCError({ 42 | message, 43 | code, 44 | cause, 45 | }); 46 | }; 47 | 48 | export class CustomLimitError extends Error { 49 | constructor(message?: string) { 50 | super(message); 51 | this.name = this.constructor.name; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/types/apiTypes.ts: -------------------------------------------------------------------------------- 1 | // Define Hook Types 2 | export enum AuthorizationType { 3 | PERSONAL = "PERSONAL", 4 | ORGANIZATION = "ORGANIZATION", 5 | } 6 | -------------------------------------------------------------------------------- /src/types/backupRestore.ts: -------------------------------------------------------------------------------- 1 | export interface BackupMetadata { 2 | docker?: boolean; 3 | version?: string; 4 | timestamp?: string; 5 | // biome-ignore lint/suspicious/noExplicitAny: 6 | [key: string]: any; 7 | } 8 | -------------------------------------------------------------------------------- /src/types/central/controllerStatus.d.ts: -------------------------------------------------------------------------------- 1 | export interface CentralControllerStatus { 2 | id: string; 3 | type: string; 4 | online: boolean; 5 | clock: number; 6 | version: string; 7 | apiVersion: string; 8 | uptime: number; 9 | secondFactor: boolean; 10 | stripePublishableKey: string; 11 | clusterNode: string; 12 | supportEmbedCode: string; 13 | loginHtmlBlurb: string; 14 | user: User; 15 | loginMethods: LoginMethods; 16 | readOnlyMode: boolean; 17 | oidcConfig: OIDCConfig; 18 | features: Features; 19 | defaultLimits: DefaultLimits; 20 | } 21 | 22 | interface User { 23 | id: string; 24 | type: string; 25 | creationTime: number; 26 | globalPermissions: GlobalPermissions; 27 | displayName: string; 28 | email: string; 29 | auth: Auth; 30 | tokens: string[]; 31 | accountLimits: AccountLimits; 32 | marketingOptIn: boolean; 33 | } 34 | 35 | interface GlobalPermissions { 36 | a: boolean; 37 | d: boolean; 38 | m: boolean; 39 | r: boolean; 40 | } 41 | 42 | interface Auth { 43 | oidc: string; 44 | } 45 | 46 | interface AccountLimits { 47 | enabled: boolean; 48 | billingV2: boolean; 49 | maxMembers: number; 50 | currentMembers: number; 51 | maxAdmins: number; 52 | currentAdmins: number; 53 | maxSSO: number; 54 | currentSSO: number; 55 | } 56 | 57 | interface LoginMethods { 58 | local: boolean; 59 | google: boolean; 60 | twitter: boolean; 61 | facebook: boolean; 62 | github: boolean; 63 | saml: boolean; 64 | oidc: boolean; 65 | } 66 | 67 | interface OIDCConfig { 68 | manageAccountURL: string; 69 | logoutURL: string; 70 | } 71 | 72 | interface Features { 73 | ztAudit: boolean; 74 | ztBillingV2: boolean; 75 | ztMemberLimits: boolean; 76 | ztThirdPartyOIDCSSO: boolean; 77 | } 78 | 79 | interface DefaultLimits { 80 | maxMembers: number; 81 | maxAdmins: number; 82 | maxSSO: number; 83 | } 84 | -------------------------------------------------------------------------------- /src/types/central/members.d.ts: -------------------------------------------------------------------------------- 1 | import { type MemberEntity } from "../local/member"; 2 | 3 | interface CentralMemberConfig { 4 | activeBridge: boolean; 5 | address: string; 6 | authorized: boolean; 7 | capabilities: number[]; 8 | creationTime: number; 9 | id: string; 10 | identity: string; 11 | ipAssignments: string[]; 12 | lastAuthorizedTime: number; 13 | lastDeauthorizedTime: number; 14 | noAutoAssignIps: boolean; 15 | nwid: string; 16 | objtype: string; 17 | remoteTraceLevel: number; 18 | remoteTraceTarget: string; 19 | revision: number; 20 | tags: number[][]; 21 | vMajor: number; 22 | vMinor: number; 23 | vRev: number; 24 | vProto: number; 25 | ssoExempt: boolean; 26 | } 27 | 28 | export interface FlattenCentralMembers extends MemberEntity, CentralMemberConfig {} 29 | 30 | export interface CentralMemberEntity extends MemberEntity { 31 | description: string; 32 | totalMemberCount: number; 33 | } 34 | -------------------------------------------------------------------------------- /src/types/ctx.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import { Session } from "next-auth"; 3 | 4 | export interface UserContext { 5 | session: Session; 6 | prisma: PrismaClient; 7 | } 8 | -------------------------------------------------------------------------------- /src/types/errorHandling.d.ts: -------------------------------------------------------------------------------- 1 | interface ZodErrorFieldErrors { 2 | updateParams?: string; 3 | } 4 | 5 | interface ZodError { 6 | fieldErrors?: ZodErrorFieldErrors; 7 | } 8 | 9 | interface ErrorData { 10 | zodError?: ZodError; 11 | } 12 | 13 | interface ShapeError { 14 | data?: ErrorData; 15 | } 16 | 17 | interface CustomBackendError { 18 | error?: string; 19 | line?: number; 20 | } 21 | 22 | export interface CustomError { 23 | shape?: ShapeError; 24 | message?: CustomBackendError; 25 | } 26 | -------------------------------------------------------------------------------- /src/types/invitation.ts: -------------------------------------------------------------------------------- 1 | import { Invitation } from "@prisma/client"; 2 | 3 | export type InvitationLinkType = Invitation & { groupName: string | null }; 4 | -------------------------------------------------------------------------------- /src/types/network.d.ts: -------------------------------------------------------------------------------- 1 | import { type MemberEntity } from "./local/member"; 2 | import { type NetworkEntity } from "./local/network"; 3 | import { type FlattenCentralNetwork } from "./central/network"; 4 | export interface NetworkAndMemberResponse { 5 | network: NetworkEntity | FlattenCentralNetwork; 6 | members: Partial; 7 | zombieMembers?: MemberEntity[]; 8 | } 9 | -------------------------------------------------------------------------------- /src/types/worldConfig.d.ts: -------------------------------------------------------------------------------- 1 | export interface WorldConfig { 2 | rootNodes: Array<{ 3 | comments: string; 4 | identity: string; 5 | endpoints: string[]; 6 | }>; 7 | signing: string[]; 8 | output: string; 9 | plID: number; 10 | plBirth: number; 11 | plRecommend: boolean; 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/IPv6.ts: -------------------------------------------------------------------------------- 1 | export const sixPlane = (networkId: string, nodeId: string): string => { 2 | const splitBy2Re = /.{1,2}/g; 3 | const splitBy4Re = /.{1,4}/g; 4 | const bytes = networkId.match(splitBy2Re); 5 | 6 | if (!bytes) return null; 7 | 8 | const networkPart = bytes 9 | .map((substr, idx, arr) => parseInt(substr, 16) ^ parseInt(arr[idx + 4], 16)) 10 | .map((byte) => byte.toString(16).toLowerCase()) 11 | .map((byte) => (byte.length === 2 ? byte : `0${byte}`)) 12 | .slice(0, 4); 13 | 14 | const nodeBytes = nodeId.match(splitBy2Re); 15 | if (!nodeBytes) return null; 16 | 17 | const nodePart = nodeBytes.slice(0, 5); 18 | 19 | const result = ["fc"] 20 | .concat(networkPart) 21 | .concat(nodePart) 22 | .concat(["00", "00", "00", "00", "00", "01"]) 23 | .join("") 24 | .match(splitBy4Re) 25 | .join(":"); 26 | 27 | return result; 28 | }; 29 | 30 | export const toRfc4193Ip = (networkId: string, memberId: string): string => { 31 | const result = `fd${networkId}9993${memberId}`.match(/.{1,4}/g); 32 | 33 | if (!result) return null; 34 | 35 | return result.join(":"); 36 | }; 37 | -------------------------------------------------------------------------------- /src/utils/docker.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | export const isRunningInDocker = () => { 4 | try { 5 | return ( 6 | fs.readFileSync("/proc/1/cgroup", "utf8").includes("docker") || 7 | fs.existsSync("/.dockerenv") 8 | ); 9 | } catch (_e) { 10 | return false; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/downloadFile.ts: -------------------------------------------------------------------------------- 1 | export const downloadFile = (data: string, fileName: string) => { 2 | const element = document.createElement("a"); 3 | const file = new Blob([data], { type: "text/plain" }); 4 | element.href = URL.createObjectURL(file); 5 | element.download = fileName; 6 | document.body.appendChild(element); 7 | element.click(); 8 | }; 9 | -------------------------------------------------------------------------------- /src/utils/enums.ts: -------------------------------------------------------------------------------- 1 | // Extracted mail enums as it caused issues if imported directly from mail.ts 2 | export enum MailTemplateKey { 3 | InviteUser = "inviteUserTemplate", 4 | InviteAdmin = "inviteAdminTemplate", 5 | InviteOrganization = "inviteOrganizationTemplate", 6 | ForgotPassword = "forgotPasswordTemplate", 7 | VerifyEmail = "verifyEmailTemplate", 8 | Notification = "notificationTemplate", 9 | NewDeviceNotification = "newDeviceNotificationTemplate", 10 | DeviceIpChangeNotification = "deviceIpChangeNotificationTemplate", 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/global.ts: -------------------------------------------------------------------------------- 1 | // Get the title from the environment variable NEXT_PUBLIC_SITE_NAME. 2 | 3 | import { env } from "~/env.mjs"; 4 | 5 | export const globalSiteVersion = env.NEXT_PUBLIC_APP_VERSION || "development"; 6 | -------------------------------------------------------------------------------- /src/utils/isIpInsubnet.ts: -------------------------------------------------------------------------------- 1 | type Route = { 2 | target?: string; 3 | }; 4 | // make a function that check if an ip is in a subnet 5 | export const isIPInSubnet = (ip: string, targets: Route[] | undefined): boolean => { 6 | if (!Array.isArray(targets)) { 7 | // console.warn( 8 | // "isIPInSubnet function expects 'targets' to be an array of strings. Invalid input received." 9 | // ); 10 | return false; 11 | } 12 | 13 | const ipToInt = (ip: string): number => { 14 | const [a, b, c, d] = ip.split(".").map(Number); 15 | return (a << 24) | (b << 16) | (c << 8) | d; 16 | }; 17 | 18 | const ipInt = ipToInt(ip); 19 | if (!targets.some((targetObj) => "target" in targetObj)) { 20 | return false; 21 | } 22 | 23 | for (const { target } of targets) { 24 | if (!target) continue; 25 | const [subnet, mask] = target.split("/"); 26 | const subnetBits = parseInt(mask, 10); 27 | 28 | const subnetInt = ipToInt(subnet); 29 | const maskInt = ~((1 << (32 - subnetBits)) - 1); 30 | 31 | if ((subnetInt & maskInt) === (ipInt & maskInt)) { 32 | return true; 33 | } 34 | } 35 | 36 | return false; 37 | }; 38 | -------------------------------------------------------------------------------- /src/utils/localstorage.ts: -------------------------------------------------------------------------------- 1 | export const setLocalStorageItem = (key: string, value: T): void => { 2 | const existingConfig = localStorage.getItem("ztnet_config"); 3 | const config = existingConfig ? JSON.parse(existingConfig) : {}; 4 | config[key] = value; 5 | localStorage.setItem("ztnet_config", JSON.stringify(config)); 6 | }; 7 | 8 | export const getLocalStorageItem = (key: string, defaultValue: T): T => { 9 | const existingConfig = localStorage.getItem("ztnet_config"); 10 | const config = existingConfig ? JSON.parse(existingConfig) : {}; 11 | return config[key] ? config[key] : defaultValue; 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/planet.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { ZT_FOLDER } from "./ztApi"; 3 | 4 | interface LocalConf { 5 | settings?: { 6 | primaryPort?: number; 7 | secondaryPort?: number; 8 | allowSecondaryPort?: boolean; 9 | }; 10 | } 11 | 12 | /* 13 | * 14 | * Update local.conf file with the new port number 15 | * 16 | */ 17 | export const updateLocalConf = (portNumbers: number[]): Promise => { 18 | return new Promise((resolve, reject) => { 19 | const localConfPath = `${ZT_FOLDER}/local.conf`; 20 | let localConf: LocalConf; 21 | 22 | try { 23 | const localConfContent = fs.readFileSync(localConfPath, "utf8"); 24 | localConf = localConfContent ? JSON.parse(localConfContent) : null; 25 | } catch (err) { 26 | if (err.code === "ENOENT") { 27 | localConf = { 28 | settings: { 29 | primaryPort: 9993, 30 | }, 31 | }; 32 | } else { 33 | reject(`Error reading zerotier-one/local.conf: ${err.message}`); 34 | return; 35 | } 36 | } 37 | if (localConf?.settings && "primaryPort" in localConf.settings && portNumbers) { 38 | localConf.settings.primaryPort = portNumbers[0]; 39 | 40 | if (portNumbers.length > 1) { 41 | localConf.settings.secondaryPort = portNumbers[1]; 42 | localConf.settings.allowSecondaryPort = true; 43 | } else { 44 | // remove the secondaryPort and allowSecondaryPort keys from local.conf 45 | const { secondaryPort, allowSecondaryPort, ...restSettings } = localConf.settings; 46 | localConf.settings = restSettings; 47 | } 48 | fs.writeFileSync(localConfPath, JSON.stringify(localConf, null, 2)); 49 | resolve(true); 50 | } else { 51 | reject('Error: "primaryPort" key does not exist in zerotier-one/local.conf file'); 52 | } 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /src/utils/rateLimit.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiResponse } from "next"; 2 | import { LRUCache } from "lru-cache"; 3 | 4 | type Options = { 5 | uniqueTokenPerInterval?: number; 6 | interval?: number; 7 | }; 8 | 9 | export default function rateLimit(options?: Options) { 10 | const tokenCache = new LRUCache({ 11 | max: options?.uniqueTokenPerInterval || 500, 12 | ttl: options?.interval || 60000, 13 | }); 14 | 15 | return { 16 | check: (res: NextApiResponse, limit: number, token: string) => 17 | new Promise((resolve, reject) => { 18 | const tokenCount = (tokenCache.get(token) as number[]) || [0]; 19 | if (tokenCount[0] === 0) { 20 | tokenCache.set(token, tokenCount); 21 | } 22 | tokenCount[0] += 1; 23 | 24 | const currentUsage = tokenCount[0]; 25 | const isRateLimited = currentUsage >= limit; 26 | res.setHeader("X-RateLimit-Limit", limit); 27 | res.setHeader("X-RateLimit-Remaining", isRateLimited ? 0 : limit - currentUsage); 28 | 29 | return isRateLimited ? reject() : resolve(); 30 | }), 31 | reset: () => tokenCache.clear(), 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/time.ts: -------------------------------------------------------------------------------- 1 | export const timeAgoFormatter = (value: number, unit: string, suffix: string) => { 2 | // Map full unit names to their abbreviations 3 | const unitAbbreviations: { [key: string]: string } = { 4 | second: "s", 5 | minute: "m", 6 | hour: "h", 7 | day: "d", 8 | week: "w", 9 | month: "mo", 10 | year: "yr", 11 | }; 12 | 13 | const abbreviation = unitAbbreviations[unit] || unit; 14 | 15 | if (suffix === "ago") { 16 | return `${value} ${abbreviation} ago`; 17 | } 18 | if (suffix === "from now") { 19 | return `in ${value} ${abbreviation}`; 20 | } 21 | return `${value} ${abbreviation} ${suffix}`; 22 | }; 23 | -------------------------------------------------------------------------------- /src/utils/webhook.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "~/server/db"; 2 | import { HookBase } from "~/types/webhooks"; 3 | 4 | // Generic function to send a webhook 5 | export const sendWebhook = async (data: T): Promise => { 6 | if (!data?.organizationId) return; 7 | 8 | const webhookData = await prisma.webhook.findMany({ 9 | where: { organizationId: data.organizationId }, 10 | }); 11 | 12 | for (const webhook of webhookData) { 13 | if ((webhook.eventTypes as string[]).includes(data.hookType)) { 14 | (async () => { 15 | try { 16 | const response = await fetch(webhook.url, { 17 | method: "POST", 18 | headers: { "Content-Type": "application/json" }, 19 | body: JSON.stringify(data), 20 | }); 21 | 22 | if (!response.ok) { 23 | console.error( 24 | `Failed to send webhook: ${response.status} ${response.statusText}`, 25 | ); 26 | } 27 | } catch (error) { 28 | console.error(`Error sending webhooks: ${error.message}`); 29 | } 30 | })(); 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | const config = { 3 | content: ["./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: { 6 | animation: { 7 | fadeIn: "fadeIn 0.2s ease-in-out", 8 | }, 9 | }, 10 | }, 11 | //@ts-ignore 12 | plugins: [require("daisyui")], 13 | daisyui: { 14 | themes: [ 15 | "dark", 16 | "light", 17 | "black", 18 | "business", 19 | "forest", 20 | "sunset", 21 | "luxury", 22 | "night", 23 | "dim", 24 | "cyberpunk", 25 | ], 26 | }, 27 | }; 28 | 29 | module.exports = config; 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "checkJs": true, 11 | "skipLibCheck": true, 12 | "strict": false, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmit": true, 15 | "esModuleInterop": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "preserve", 21 | "incremental": true, 22 | "noUncheckedIndexedAccess": true, 23 | "baseUrl": ".", 24 | "paths": { 25 | "~/*": [ 26 | "./src/*" 27 | ] 28 | } 29 | }, 30 | "include": [ 31 | "./src/global.d.ts", 32 | ".eslintrc.cjs", 33 | "next-env.d.ts", 34 | "**/*.ts", 35 | "**/*.tsx", 36 | "**/*.cjs", 37 | "**/*.mjs", 38 | ], 39 | "exclude": [ 40 | "node_modules", 41 | "docs", 42 | "install.ztnet", 43 | ".next" 44 | ] 45 | } -------------------------------------------------------------------------------- /ztnodeid/Dockerfile: -------------------------------------------------------------------------------- 1 | ##### 2 | # 3 | # Generate mkworld binaries 4 | # !needs binfmt: 5 | # docker run --privileged --rm tonistiigi/binfmt --install all 6 | # 7 | # run as: 8 | # docker buildx build --pull --rm --platform linux/amd64,linux/arm64 --target export-stage -o type=local,dest=./build . 9 | ###### 10 | 11 | # BUILD GO UTILS 12 | FROM golang:bullseye AS gobuilder 13 | ARG TARGETPLATFORM 14 | WORKDIR /buildsrc 15 | COPY . /buildsrc 16 | 17 | ENV CGO_ENABLED=0 18 | RUN apt update -y && \ 19 | apt install zip -y 20 | 21 | RUN go build -ldflags='-s -w' -trimpath -o binaries/ztmkworld cmd/mkworld/main.go 22 | # RUN GOOS=freebsd GOARCH=amd64 go build -ldflags='-s -w' -trimpath -o binaries/ztmkworld cmd/mkworld/main.go 23 | 24 | FROM scratch AS export-stage 25 | COPY --from=gobuilder /buildsrc/binaries . -------------------------------------------------------------------------------- /ztnodeid/assets/mkworld.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "rootNodes": [ 3 | { 4 | "comments": "amsterdam official", 5 | "identity": "992fcf1db7:0:206ed59350b31916f749a1f85dffb3a8787dcbf83b8c6e9448d4e3ea0e3369301be716c3609344a9d1533850fb4460c50af43322bcfc8e13d3301a1f1003ceb6", 6 | "endpoints": [ 7 | "195.181.173.159/443", 8 | "2a02:6ea0:c024::/443" 9 | ] 10 | } 11 | ], 12 | "signing": [ 13 | "previous.c25519", 14 | "current.c25519" 15 | ], 16 | "output": "planet.custom", 17 | "plID": 0, 18 | "plBirth": 0, 19 | "plRecommend": true 20 | } -------------------------------------------------------------------------------- /ztnodeid/build/freebsd_amd64/ztmkworld: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/ztnodeid/build/freebsd_amd64/ztmkworld -------------------------------------------------------------------------------- /ztnodeid/build/linux_amd64/ztmkworld: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/ztnodeid/build/linux_amd64/ztmkworld -------------------------------------------------------------------------------- /ztnodeid/build/linux_arm64/ztmkworld: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinamics/ztnet/5980fd49b195716f11647df407035d416cce41dc/ztnodeid/build/linux_arm64/ztmkworld -------------------------------------------------------------------------------- /ztnodeid/go.mod: -------------------------------------------------------------------------------- 1 | module ztnodeid 2 | 3 | go 1.20 4 | 5 | require golang.org/x/crypto v0.35.0 6 | -------------------------------------------------------------------------------- /ztnodeid/go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 2 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 3 | -------------------------------------------------------------------------------- /ztnodeid/pkg/node/errs.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: AGPL-3.0-only 3 | * Copyright (C) 2023 by kmahyyg in Patmeow Limited 4 | */ 5 | 6 | package node 7 | 8 | import "errors" 9 | 10 | var ( 11 | ErrMaxEndpointsExceeded = errors.New("zerotier node has too many endpoints") 12 | ErrMaxRootsExceeded = errors.New("zerotier root exceeds limits") 13 | ErrSerializedDataTooLarge = errors.New("serialized data longer than restriction") 14 | ErrInvalidData = errors.New("data input invalid") 15 | ErrUnknown = errors.New("unknown error") 16 | ) 17 | --------------------------------------------------------------------------------