diff --git a/src/components/modules/sidebar.tsx b/src/components/modules/sidebar.tsx index 45e29e88..dc6c7b04 100644 --- a/src/components/modules/sidebar.tsx +++ b/src/components/modules/sidebar.tsx @@ -167,6 +167,40 @@ const Sidebar = (): JSX.Element => { ZT Controller +
  • + + + + + + + + Settings + +
  • ) : null}
  • diff --git a/src/pages/admin/controller/index.tsx b/src/pages/admin/controller/index.tsx index 53adc091..7cece577 100644 --- a/src/pages/admin/controller/index.tsx +++ b/src/pages/admin/controller/index.tsx @@ -1,18 +1,75 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { clearConfigCache } from "prettier"; import { type ReactElement } from "react"; import { LayoutAuthenticated } from "~/components/layouts/layout"; import { api } from "~/utils/api"; const Controller = () => { - const { data: controllerStats } = api.admin.getControllerStats.useQuery({}); + const { data: controllerData, isLoading } = + api.admin.getControllerStats.useQuery(); + + if (isLoading) { + return
    Loading...
    ; + } + + const { networkCount, totalMembers, controllerStatus } = controllerData; + + const { allowManagementFrom, allowTcpFallbackRelay, listeningOn } = + controllerStatus?.config?.settings; + + const { online, tcpFallbackActive, version } = controllerStatus; + return (
    - Controller -
    {JSON.stringify(controllerStats, null, 2)}
    +
    +
    +
    +
    +

    Networks

    +

    Network Count: {networkCount}

    +

    Total Members: {totalMembers}

    +
    +
    +
    +
    +

    Controller TCP

    +

    Allow Management From:

    +
      + {allowManagementFrom.map((address, index) => ( +
    • {address}
    • + ))} +
    +

    + Allow TCP Fallback Relay: {allowTcpFallbackRelay ? "Yes" : "No"} +

    +

    Listening On:

    +
      + {listeningOn.map((address, index) => ( +
    • {address}
    • + ))} +
    +
    +
    + +
    +
    +

    + Controller Stats +

    +

    Online: {online ? "Yes" : "No"}

    +

    + TCP Fallback Active: {tcpFallbackActive ? "Yes" : "No"} +

    +

    Version: {version}

    +
    +
    +
    +
    ); }; - Controller.getLayout = function getLayout(page: ReactElement) { return {page}; }; + export default Controller; diff --git a/src/pages/admin/settings/index.tsx b/src/pages/admin/settings/index.tsx new file mode 100644 index 00000000..c622e11c --- /dev/null +++ b/src/pages/admin/settings/index.tsx @@ -0,0 +1,45 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { clearConfigCache } from "prettier"; +import { type ReactElement } from "react"; +import { LayoutAuthenticated } from "~/components/layouts/layout"; +import { api } from "~/utils/api"; + +const Settings = () => { + const { mutate: setOptions } = api.options.update.useMutation(); + + const { data: options, refetch: refetchOptions } = + api.options.getAll.useQuery(); + + return ( +
    +
    +
    +
    +
    +

    Site Settings

    +
    +

    Enable user registration?

    + ) => { + setOptions( + { enableRegistration: e.target.checked }, + { onSuccess: () => void refetchOptions() } + ); + }} + /> +
    +
    +
    +
    +
    +
    + ); +}; +Settings.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default Settings; diff --git a/src/server/api/root.ts b/src/server/api/root.ts index 6dd0c8b8..4af49c76 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -3,6 +3,7 @@ import { authRouter } from "./routers/authRouter"; import { networkMemberRouter } from "./routers/networkMemberRouter"; import { networkRouter } from "./routers/networkRouter"; import { adminRouter } from "./routers/adminRoute"; +import { globalOptionsRouter } from "./routers/globalOptions"; /** * This is the primary router for your server. @@ -14,6 +15,7 @@ export const appRouter = createTRPCRouter({ networkMember: networkMemberRouter, auth: authRouter, admin: adminRouter, + options: globalOptionsRouter, }); // export type definition of API diff --git a/src/server/api/routers/adminRoute.ts b/src/server/api/routers/adminRoute.ts index eff15961..2210768d 100644 --- a/src/server/api/routers/adminRoute.ts +++ b/src/server/api/routers/adminRoute.ts @@ -33,50 +33,25 @@ export const adminRouter = createTRPCRouter({ }), getControllerStats: adminRoleProtectedRoute - .input( - z.object({ - userid: z.number().optional(), - }) - ) - .query(async ({ ctx, input }) => { - const controllerVersion = await ztController.get_controller_version(); - + // .input( + // z.object({ + // userid: z.number().optional(), + // }) + // ) + .query(async () => { const networks = await ztController.get_controller_networks(); - - const nodes = []; - let totalNodes = 0; - for (let index = 0; index < networks.length; index++) { - const networkMembers = await ctx.prisma.network.findFirst({ - where: { - nwid: networks[index], - }, - include: { - nw_userid: true, - }, - }); - - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access - const nDet = await ztController.network_detail(networks[index]); - totalNodes += nDet.members.length; - nodes.push({ ...nDet, author: { ...networkMembers } }); - } - // admin wants networks for a specific user - if (input.userid && !isNaN(input.userid)) { - const filterNodes = nodes.filter( - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any - (u: any) => u.author.authorId === input.userid - ); - return { - controllerVersion, - nodes: filterNodes, - stats: { totalNodes, totalNetworks: nodes.length }, - }; + const networkCount = networks.length; + let totalMembers = 0; + for (const network of networks) { + const members = await ztController.network_members(network); + totalMembers += Object.keys(members).length; } + const controllerStatus = await ztController.get_controller_status(); return { - controllerVersion, - nodes, - stats: { totalNodes, totalNetworks: nodes.length }, + networkCount, + totalMembers, + controllerStatus, }; }), }); diff --git a/src/server/api/routers/authRouter.ts b/src/server/api/routers/authRouter.ts index a60915be..55589aa5 100644 --- a/src/server/api/routers/authRouter.ts +++ b/src/server/api/routers/authRouter.ts @@ -57,7 +57,7 @@ export const authRouter = createTRPCRouter({ if (!settings.enableRegistration) { throw new TRPCError({ code: "BAD_REQUEST", - message: `Registration is disabled!`, + message: `Registration is disabled! Please contact the administrator.`, }); } diff --git a/src/server/api/routers/example.ts b/src/server/api/routers/example.ts deleted file mode 100644 index 34564bb9..00000000 --- a/src/server/api/routers/example.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { z } from "zod"; - -import { - createTRPCRouter, - publicProcedure, - protectedProcedure, -} from "~/server/api/trpc"; - -export const exampleRouter = createTRPCRouter({ - hello: publicProcedure - .input(z.object({ text: z.string() })) - .query(({ input }) => { - return { - greeting: `Hello ${input.text}`, - }; - }), - - // getAll: publicProcedure.query(({ ctx }) => { - // return ctx.prisma.example.findMany(); - // }), - - getSecretMessage: protectedProcedure.query(() => { - return "you can now see this secret message!"; - }), -}); diff --git a/src/server/api/routers/globalOptions.ts b/src/server/api/routers/globalOptions.ts new file mode 100644 index 00000000..f92be89b --- /dev/null +++ b/src/server/api/routers/globalOptions.ts @@ -0,0 +1,29 @@ +import { z } from "zod"; +import { createTRPCRouter, adminRoleProtectedRoute } from "~/server/api/trpc"; + +export const globalOptionsRouter = createTRPCRouter({ + update: adminRoleProtectedRoute + .input( + z.object({ + enableRegistration: z.boolean().optional(), + firstUserRegistration: z.boolean().optional(), + }) + ) + .mutation(async ({ ctx, input }) => { + return await ctx.prisma.globalOptions.update({ + where: { + id: 1, + }, + data: { + ...input, + }, + }); + }), + getAll: adminRoleProtectedRoute.query(async ({ ctx }) => { + return await ctx.prisma.globalOptions.findFirst({ + where: { + id: 1, + }, + }); + }), +}); diff --git a/src/utils/ztApi.ts b/src/utils/ztApi.ts index f2d70bf3..3c14193f 100644 --- a/src/utils/ztApi.ts +++ b/src/utils/ztApi.ts @@ -80,6 +80,7 @@ export const get_controller_networks = Node status and addressing info https://docs.zerotier.com/service/v1/#operation/getStatus */ + export interface ZTControllerNodeStatus { address: string; clock: number; @@ -101,12 +102,20 @@ export interface Config { } export interface Settings { + allowManagementFrom: null[]; allowTcpFallbackRelay: boolean; + forceTcpRelay: boolean; + listeningOn: null[]; portMappingEnabled: boolean; primaryPort: number; + secondaryPort: number; + softwareUpdate: string; + softwareUpdateChannel: string; + surfaceAddresses: null[]; + tertiaryPort: number; } -export const get_zt_address = async function () { +export const get_controller_status = async function () { try { const { data } = await axios.get(ZT_ADDR + "/status", options); return data as ZTControllerNodeStatus; @@ -166,7 +175,7 @@ export const network_create = async ( name, ipAssignment ): Promise => { - const zt_address = await get_zt_address(); + const controllerStatus = await get_controller_status(); const config = { name, @@ -175,7 +184,7 @@ export const network_create = async ( try { const response: AxiosResponse = await axios.post( - `${ZT_ADDR}/controller/network/${zt_address.address}______`, + `${ZT_ADDR}/controller/network/${controllerStatus.address}______`, config, options ); @@ -229,25 +238,53 @@ export async function network_delete( // Get Network Member Details by ID // https://docs.zerotier.com/service/v1/#operation/getControllerNetworkMember +type MemberRevisionCounters = { + [memberId: string]: number; +}; +export const network_members = async function ( + nwid: string +): Promise { + try { + const members: AxiosResponse = await axios.get( + `${ZT_ADDR}/controller/network/${nwid}/member/`, + options + ); + + return members.data; + } catch (error) { + if (axios.isAxiosError(error)) { + const axiosError = error as AxiosError; + // eslint-disable-next-line no-console + console.error(`Axios error: ${axiosError.message}`); + // eslint-disable-next-line no-console + console.error(`Status code: ${axiosError.response?.status}`); + // eslint-disable-next-line no-console + console.error(`Status text: ${axiosError.response?.statusText}`); + throw axiosError; + } + // eslint-disable-next-line no-console + console.error(`Unknown error: ${error.message}`); + throw error; + } +}; + type ZTControllerResponse = { network: ZtControllerNetwork; members: MembersEntity[]; }; - export const network_detail = async function ( nwid: string ): Promise { try { - const members: AxiosResponse = await axios.get( - `${ZT_ADDR}/controller/network/${nwid}/member/`, - options - ); + // get all members for a specific network + const members = network_members(nwid); + const network: AxiosResponse = await axios.get( `${ZT_ADDR}/controller/network/${nwid}`, options ); const membersArr: any = []; - for (const member in members.data) { + for (const member in members) { const memberDetails: AxiosResponse = await axios.get( `${ZT_ADDR}/controller/network/${nwid}/member/${member}`, options