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