diff --git a/src/components/modules/memberOptionsModal.tsx b/src/components/modules/memberOptionsModal.tsx index 0475f1f5..39a2ea4a 100644 --- a/src/components/modules/memberOptionsModal.tsx +++ b/src/components/modules/memberOptionsModal.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { api } from "~/utils/api"; import { toast } from "react-hot-toast"; import { useRouter } from "next/router"; @@ -5,17 +6,15 @@ import { isIPInSubnet } from "~/utils/isIpInsubnet"; import cn from "classnames"; import { useState, useEffect } from "react"; import { type Prisma } from "@prisma/client"; -import { - type TagDetails, - type CapabilitiesByName, - type TagsByName, -} from "~/types/network"; import Anotation from "./anotation"; import { useTranslations } from "next-intl"; +import { type CapabilitiesByName, type TagDetails } from "~/types/local/member"; +import { type TagsByName } from "~/types/local/network"; interface ModalContentProps { nwid: string; memberId: string; + central: boolean; // ipAssignments: string[]; } const initialIpState = { ipInput: "", isValid: false }; @@ -23,6 +22,7 @@ const initialIpState = { ipInput: "", isValid: false }; export const MemberOptionsModal: React.FC = ({ nwid, memberId, + central = false, }) => { const t = useTranslations("networkById"); const [state, setState] = useState(initialIpState); @@ -33,6 +33,7 @@ export const MemberOptionsModal: React.FC = ({ api.network.getNetworkById.useQuery( { nwid, + central, }, { enabled: !!query.id, networkMode: "online" } ); @@ -41,6 +42,7 @@ export const MemberOptionsModal: React.FC = ({ { nwid, id: memberId, + central, }, { enabled: !!query.id, networkMode: "online" } ); @@ -48,9 +50,11 @@ export const MemberOptionsModal: React.FC = ({ useEffect(() => { // find member by id const member = networkById?.members.find( + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access (member) => member.id === memberId ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access seIpAssignments(member?.ipAssignments || []); }, [networkById?.members, memberId]); @@ -88,6 +92,7 @@ export const MemberOptionsModal: React.FC = ({ { updateParams: { ipAssignments: [...newIpPool] }, memberId: id, + central, nwid, }, { @@ -139,6 +144,7 @@ export const MemberOptionsModal: React.FC = ({ { updateParams: { ipAssignments: [...ipAssignments, ipInput] }, memberId, + central, nwid, }, { @@ -176,6 +182,7 @@ export const MemberOptionsModal: React.FC = ({ capabilities, }, memberId, + central, nwid, }, { @@ -252,6 +259,7 @@ export const MemberOptionsModal: React.FC = ({ tags, }, memberId, + central, nwid, }, { @@ -425,6 +433,7 @@ export const MemberOptionsModal: React.FC = ({ activeBridge: e.target.checked, }, memberId, + central, nwid, }, { @@ -454,6 +463,7 @@ export const MemberOptionsModal: React.FC = ({ noAutoAssignIps: e.target.checked, }, memberId, + central, nwid, }, { @@ -477,11 +487,13 @@ export const MemberOptionsModal: React.FC = ({ {TagDropdowns(networkById?.network?.tagsByName)} -
-
- + {!central ? ( +
+
+ +
-
+ ) : null}
); diff --git a/src/components/modules/networkMembersTable.tsx b/src/components/modules/networkMembersTable.tsx index f0ecffa7..9f490485 100644 --- a/src/components/modules/networkMembersTable.tsx +++ b/src/components/modules/networkMembersTable.tsx @@ -19,16 +19,16 @@ import { useRouter } from "next/router"; import { isIPInSubnet } from "~/utils/isIpInsubnet"; import { useModalStore } from "~/utils/store"; import { MemberOptionsModal } from "./memberOptionsModal"; -import { - type NetworkMemberNotation, - type MembersEntity, -} from "~/types/network"; import { DebouncedInput } from "../elements/debouncedInput"; import { useSkipper } from "../elements/useSkipper"; import TableFooter from "./tableFooter"; import { convertRGBtoRGBA } from "~/utils/randomColor"; import Input from "../elements/input"; import { useTranslations } from "next-intl"; +import { + type MemberEntity, + type NetworkMemberNotation, +} from "~/types/local/member"; // import { makeNetworkMemberData } from "~/utils/fakeData"; declare module "@tanstack/react-table" { @@ -40,7 +40,7 @@ declare module "@tanstack/react-table" { interface IProp { nwid: string; - central?: boolean; + central: boolean; } enum ConnectionStatus { @@ -99,6 +99,7 @@ export const NetworkMembersTable = ({ nwid, central = false }: IProp) => { updateParams: { ipAssignments: [...newIpPool] }, memberId: id, nwid, + central, }, { onSuccess: () => { @@ -116,8 +117,8 @@ export const NetworkMembersTable = ({ nwid, central = false }: IProp) => { { onSuccess: () => void refetchNetworkById() } ); }; - const columnHelper = createColumnHelper(); - const columns = useMemo[]>( + const columnHelper = createColumnHelper(); + const columns = useMemo[]>( () => [ columnHelper.accessor( (row) => { @@ -149,6 +150,7 @@ export const NetworkMembersTable = ({ nwid, central = false }: IProp) => { { nwid, memberId: original.id, + central, updateParams: { authorized: event.target.checked }, }, { onSuccess: () => void refetchNetworkById() } @@ -179,15 +181,26 @@ export const NetworkMembersTable = ({ nwid, central = false }: IProp) => { columnHelper.accessor("creationTime", { header: () => {t("networkMembersTable.column.created")}, id: "creationTime", - cell: (info) => , + cell: (info) => , }), columnHelper.accessor("peers", { header: () => ( {t("networkMembersTable.column.physicalIp.header")} ), id: "physicalAddress", - cell: (info) => { - const val = info.getValue(); + cell: ({ getValue, row: { original } }) => { + if (central) { + const val = original; + if (!val || typeof val.physicalAddress !== "string") + return ( + + {t("networkMembersTable.column.physicalIp.unknownValue")} + + ); + + return val.physicalAddress.split("/")[0]; + } + const val = getValue(); if (!val || typeof val.physicalAddress !== "string") return ( @@ -207,7 +220,37 @@ export const NetworkMembersTable = ({ nwid, central = false }: IProp) => { const formatTime = (value: string, unit: string) => `${value} ${unit}`; const cursorStyle = { cursor: "pointer" }; + if (central) { + const now = Date.now(); // current timestamp in milliseconds + const lastSeen = original?.lastSeen; // assuming lastSeen is a timestamp in milliseconds + const fiveMinutesAgo = now - 5 * 60 * 1000; // timestamp 5 minutes ago + // Check if lastSeen is within the last 5 minutes + if (lastSeen >= fiveMinutesAgo) { + // The user is considered online + return ( + + ONLINE + + ); + } else { + // The user is considered offline + return ( + + {t("networkMembersTable.column.conStatus.offline")} + + + ); + } + } if (original.conStatus === ConnectionStatus.Relayed) { return ( { ? t("networkMembersTable.column.conStatus.directLan") : t("networkMembersTable.column.conStatus.directWan"); const versionInfo = - original.peers && original.peers?.version !== "-1.-1.-1" + original.peers && original?.peers?.version !== "-1.-1.-1" ? ` (v${original.peers?.version})` : ""; @@ -253,10 +296,7 @@ export const NetworkMembersTable = ({ nwid, central = false }: IProp) => { title="User is offline" > {t("networkMembersTable.column.conStatus.offline")} - + ); }, @@ -285,6 +325,7 @@ export const NetworkMembersTable = ({ nwid, central = false }: IProp) => { ), }) @@ -310,7 +351,7 @@ export const NetworkMembersTable = ({ nwid, central = false }: IProp) => { ); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const defaultColumn: Partial> = { + const defaultColumn: Partial> = { cell: ({ getValue, row: { index, original }, column: { id }, table }) => { const initialValue = getValue(); // eslint-disable-next-line react-hooks/rules-of-hooks @@ -331,6 +372,7 @@ export const NetworkMembersTable = ({ nwid, central = false }: IProp) => { { nwid, id: original.id, + central, updateParams: { name: value as string, }, @@ -478,8 +520,11 @@ export const NetworkMembersTable = ({ nwid, central = false }: IProp) => { // const [data, setData] = useState(() => makeNetworkMemberData(100)); const [autoResetPageIndex, skipAutoResetPageIndex] = useSkipper(); const table = useReactTable({ + // @ts-expect-error known error data, + // @ts-expect-error known error columns, + // @ts-expect-error known error defaultColumn, onSortingChange: setSorting, getCoreRowModel: getCoreRowModel(), @@ -499,7 +544,6 @@ export const NetworkMembersTable = ({ nwid, central = false }: IProp) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-return old.map((row, index) => { if (index === rowIndex) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion ...old[rowIndex]!, diff --git a/src/pages/central/[id].tsx b/src/pages/central/[id].tsx index 6031b018..7839c625 100644 --- a/src/pages/central/[id].tsx +++ b/src/pages/central/[id].tsx @@ -173,11 +173,7 @@ const CentralNetworkById = () => {
{members?.length ? (
- setEditing(e)} - /> +
) : (
diff --git a/src/pages/network/[id].tsx b/src/pages/network/[id].tsx index 1f554a6d..7491d0ac 100644 --- a/src/pages/network/[id].tsx +++ b/src/pages/network/[id].tsx @@ -216,10 +216,7 @@ const NetworkById = () => {
{members.length ? (
- setEditing(e)} - /> +
) : (
diff --git a/src/server/api/routers/memberRouter.ts b/src/server/api/routers/memberRouter.ts index 9e0750bc..455d8067 100644 --- a/src/server/api/routers/memberRouter.ts +++ b/src/server/api/routers/memberRouter.ts @@ -2,7 +2,8 @@ import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import * as ztController from "~/utils/ztApi"; import { TRPCError } from "@trpc/server"; -import { type NetworkAndMembers } from "~/types/network"; +import { type MemberEntity } from "~/types/local/member"; +import { type CentralMembers } from "~/types/central/members"; const isValidZeroTierNetworkId = (id: string) => { const hexRegex = /^[0-9a-fA-F]{10}$/; @@ -21,11 +22,22 @@ export const networkMemberRouter = createTRPCRouter({ getMemberById: protectedProcedure .input( z.object({ + central: z.boolean().default(false), id: z.string({ required_error: "No member id provided!" }), nwid: z.string({ required_error: "No network id provided!" }), }) ) .query(async ({ ctx, input }) => { + if (input.central) { + const memberDetails = await ztController.member_details( + input.nwid, + input.id, + input.central + ); + return ztController.flattenCentralMember( + memberDetails as CentralMembers + ); + } return await ctx.prisma.networkMembers.findFirst({ where: { id: input.id, @@ -75,7 +87,7 @@ export const networkMemberRouter = createTRPCRouter({ id: input.id, authorized: false, ipAssignments: [], - lastseen: new Date(), + lastSeen: new Date(), creationTime: new Date(), nwid_ref: { connect: { nwid: input.nwid }, @@ -89,6 +101,7 @@ export const networkMemberRouter = createTRPCRouter({ z.object({ nwid: z.string({ required_error: "No network id provided!" }), memberId: z.string({ required_error: "No member id provided!" }), + central: z.boolean().default(false), updateParams: z.object({ activeBridge: z.boolean().optional(), noAutoAssignIps: z.boolean().optional(), @@ -104,7 +117,7 @@ export const networkMemberRouter = createTRPCRouter({ }) ) .mutation(async ({ ctx, input }) => { - const payload: Partial = {}; + const payload: Partial = {}; // update capabilities if (typeof input.updateParams.capabilities === "object") { @@ -159,8 +172,18 @@ export const networkMemberRouter = createTRPCRouter({ ); } + const updateParams = input.central + ? { config: { ...payload } } + : { ...payload }; + console.log(input); + // if central is true, send the request to the central API and return the response const updatedMember = await ztController - .member_update(input.nwid, input.memberId, payload) + .member_update({ + nwid: input.nwid, + memberId: input.memberId, + central: input.central, + updateParams, + }) .catch(() => { throw new TRPCError({ message: @@ -169,6 +192,8 @@ export const networkMemberRouter = createTRPCRouter({ }); }); + if (input.central) return updatedMember; + const response = await ctx.prisma.network .update({ where: { @@ -221,6 +246,7 @@ export const networkMemberRouter = createTRPCRouter({ z.object({ nwid: z.string(), id: z.string(), + central: z.boolean().default(false), updateParams: z.object({ // ipAssignments: z // .array(z.string({ required_error: "No Ip assignment provided!" })) @@ -231,6 +257,24 @@ export const networkMemberRouter = createTRPCRouter({ }) ) .mutation(async ({ ctx, input }) => { + // if central is true, send the request to the central API and return the response + if (input.central && input?.updateParams?.name) { + return await ztController + .member_update({ + nwid: input.nwid, + memberId: input.id, + central: input.central, + updateParams: input.updateParams, + }) + .catch(() => { + throw new TRPCError({ + message: + "Member does not exsist in the network, did you add this device manually? \r\n Make sure it has properly joined the network", + code: "FORBIDDEN", + }); + }); + } + // if users click the re-generate icon on IP address const response = await ctx.prisma.network.update({ where: { diff --git a/src/types/central/members.d.ts b/src/types/central/members.d.ts index e1711ff4..e8c07b71 100644 --- a/src/types/central/members.d.ts +++ b/src/types/central/members.d.ts @@ -35,7 +35,7 @@ export interface CentralMembers { description: string; config?: CentralMemberConfig; lastOnline: number; - lastSeen: number; + lastSeen?: number; physicalAddress: string; physicalLocation: null | string; // Assuming this can be string in some cases clientVersion: string; diff --git a/src/types/local/member.d.ts b/src/types/local/member.d.ts index 112486de..7693d318 100644 --- a/src/types/local/member.d.ts +++ b/src/types/local/member.d.ts @@ -1,5 +1,7 @@ // Member Related Types export interface MemberEntity { + id: string; + name: string; activeBridge: boolean; address: string; nodeId?: string; @@ -22,10 +24,16 @@ export interface MemberEntity { revision: number; ssoExempt: boolean; tags: Tag[]; + peers: Peers; + lastSeen?: number; + conStatus?: number; vMajor: number; vMinor: number; vProto: number; vRev: number; + action: null; + notations?: Notation[]; + physicalAddress?: string; } export interface Peers { diff --git a/src/types/local/network.d.ts b/src/types/local/network.d.ts index c3246ffc..e280ccca 100644 --- a/src/types/local/network.d.ts +++ b/src/types/local/network.d.ts @@ -29,8 +29,12 @@ export interface NetworkEntity { ssoEnabled?: boolean; cidr?: string[]; tagsByName?: TagsByName; + capabilitiesByName?: CapabilitiesByName; } +interface CapabilitiesByName { + [key: string]: number; +} interface TagsByName { [tagName: string]: TagDetails; id?: number; diff --git a/src/utils/ztApi.ts b/src/utils/ztApi.ts index 772b2e68..03992b47 100644 --- a/src/utils/ztApi.ts +++ b/src/utils/ztApi.ts @@ -20,7 +20,12 @@ import { } from "~/types/ztController"; import { type CentralControllerStatus } from "~/types/central/controllerStatus"; -import { type CentralMembers } from "~/types/central/members"; +import { + FlattenCentralMembers, + type CentralMembers, + FlattenCentralMembers, + CentralMembers, +} from "~/types/central/members"; import { type CentralNetwork, type FlattenCentralNetwork, @@ -81,17 +86,19 @@ const getOptions = async (isCentral = false) => { }; }; -const flattenCentralMembers = (members: CentralMembers[]) => { - if (!members) return []; - return members.map((member) => { - // Destructure the network object into config and other properties - const { id: nodeId, config, ...otherProps } = member; - - // Merge the config object into the main network object - const flattenedMembers = { nodeId, ...config, ...otherProps }; +export const flattenCentralMember = ( + member: CentralMembers +): FlattenCentralMembers => { + const { id: nodeId, config, ...otherProps } = member; + const flattenedMember = { nodeId, ...config, ...otherProps }; + return flattenedMember; +}; - return flattenedMembers; - }); +export const flattenCentralMembers = ( + members: CentralMembers[] +): FlattenCentralMembers[] => { + if (!members) return []; + return members.map((member) => flattenCentralMember(member)); }; export const flattenNetwork = ( @@ -463,14 +470,21 @@ export const member_delete = async ({ } }; +type memberUpdate = { + nwid: string; + memberId: string; + updateParams: Partial; + central?: boolean; +}; + // Update Network Member by ID // https://docs.zerotier.com/service/v1/#operation/updateControllerNetworkMember -export const member_update = async ( - nwid: string, - memberId: string, - data, - central = false -): Promise => { +export const member_update = async ({ + nwid, + memberId, + updateParams: payload, + central = false, +}: memberUpdate): Promise => { const addr = central ? `${CENTRAL_ZT_ADDR}/network/${nwid}/member/${memberId}` : `${LOCAL_ZT_ADDR}/controller/network/${nwid}/member/${memberId}`; @@ -478,8 +492,11 @@ export const member_update = async ( // get headers based on local or central api const headers = await getOptions(central); try { - const response: AxiosResponse = await axios.post(addr, data, headers); - return response.data as ZTControllerMemberUpdate; + return await postData( + addr, + headers, + payload + ); } catch (error) { const prefix = central ? "[CENTRAL] " : ""; const message = `${prefix}An error occurred while getting member_update`; @@ -487,6 +504,29 @@ export const member_update = async ( } }; +// Get Network Member Details by ID +// https://docs.zerotier.com/service/v1/#operation/getControllerNetworkMember + +export const member_details = async function ( + nwid: string, + memberId: string, + central = false +): Promise { + // get headers based on local or central api + const headers = await getOptions(central); + + try { + const addr = central + ? `${CENTRAL_ZT_ADDR}/network/${nwid}/member/${memberId}` + : `${LOCAL_ZT_ADDR}/controller/network/${nwid}/member/${memberId}`; + + return await getData(addr, headers); + } catch (error) { + const message = "An error occurred while getting member_detail"; + throw new APIError(message, error as AxiosError); + } +}; + // Get all peers // https://docs.zerotier.com/service/v1/#operation/getPeers export const peers = async (): Promise => {