From f5388989f4821059a325a95d5e941aad490bf738 Mon Sep 17 00:00:00 2001 From: Bernt Christian Egeland Date: Fri, 11 Aug 2023 07:17:36 +0200 Subject: [PATCH 1/8] redirect user to dashboard if signed in --- src/pages/index.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/pages/index.tsx b/src/pages/index.tsx index a9259324..915312e2 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -106,6 +106,15 @@ export const getServerSideProps: GetServerSideProps = async ( return { props: { messages } }; } + if (session.user) { + return { + redirect: { + destination: "/dashboard", + permanent: false, + }, + }; + } + return { props: { auth: session.user, messages }, }; From cb25e055f84900673d2916e2992f7fe1af367420 Mon Sep 17 00:00:00 2001 From: Bernt Christian Egeland Date: Fri, 11 Aug 2023 10:16:01 +0200 Subject: [PATCH 2/8] rework ztapi to get db url,key for each user --- .../20230811063619_user_options/migration.sql | 34 ++++ prisma/schema.prisma | 64 +++--- src/components/layouts/sidebar.tsx | 5 +- .../modules/table/memberEditCell.tsx | 4 +- .../modules/table/networkMembersTable.tsx | 4 +- src/pages/admin/index.tsx | 6 - src/pages/user-settings/account/index.tsx | 20 +- src/pages/user-settings/index.tsx | 6 + .../network/index.tsx | 33 +--- src/server/api/networkService.ts | 4 +- src/server/api/routers/adminRoute.ts | 49 ++--- src/server/api/routers/authRouter.ts | 100 +++++++++- src/server/api/routers/memberRouter.ts | 4 + src/server/api/routers/networkRouter.ts | 37 ++-- src/server/api/routers/settingsRouter.ts | 43 +--- src/types/ctx.ts | 7 + src/types/local/network.d.ts | 26 ++- src/types/ztController.ts | 3 + src/utils/ztApi.ts | 185 +++++++++++++----- 19 files changed, 425 insertions(+), 209 deletions(-) create mode 100644 prisma/migrations/20230811063619_user_options/migration.sql rename src/pages/{admin => user-settings}/network/index.tsx (69%) create mode 100644 src/types/ctx.ts diff --git a/prisma/migrations/20230811063619_user_options/migration.sql b/prisma/migrations/20230811063619_user_options/migration.sql new file mode 100644 index 00000000..99c1ebf4 --- /dev/null +++ b/prisma/migrations/20230811063619_user_options/migration.sql @@ -0,0 +1,34 @@ +/* + Warnings: + + - You are about to drop the column `showNotationMarkerInTableRow` on the `GlobalOptions` table. All the data in the column will be lost. + - You are about to drop the column `useNotationColorAsBg` on the `GlobalOptions` table. All the data in the column will be lost. + - You are about to drop the column `ztCentralApiKey` on the `GlobalOptions` table. All the data in the column will be lost. + - You are about to drop the column `ztCentralApiUrl` on the `GlobalOptions` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "GlobalOptions" DROP COLUMN "showNotationMarkerInTableRow", +DROP COLUMN "useNotationColorAsBg", +DROP COLUMN "ztCentralApiKey", +DROP COLUMN "ztCentralApiUrl"; + +-- CreateTable +CREATE TABLE "UserOptions" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "useNotationColorAsBg" BOOLEAN DEFAULT false, + "showNotationMarkerInTableRow" BOOLEAN DEFAULT true, + "ztCentralApiKey" TEXT DEFAULT '', + "ztCentralApiUrl" TEXT DEFAULT 'https://api.zerotier.com/api/v1', + "localControllerUrl" TEXT DEFAULT 'http://zerotier:9993', + "localControllerSecret" TEXT DEFAULT '', + + CONSTRAINT "UserOptions_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "UserOptions_userId_key" ON "UserOptions"("userId"); + +-- AddForeignKey +ALTER TABLE "UserOptions" ADD CONSTRAINT "UserOptions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2d380b10..9c83ab40 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -36,14 +36,6 @@ model GlobalOptions { // Notifications userRegistrationNotification Boolean @default(false) - - // networks - useNotationColorAsBg Boolean? @default(false) - showNotationMarkerInTableRow Boolean? @default(true) - - //zt central - ztCentralApiKey String? @default("") - ztCentralApiUrl String? @default("https://api.zerotier.com/api") } enum Role { @@ -133,25 +125,45 @@ model Session { user User @relation(fields: [userId], references: [id], onDelete: Cascade) } +model UserOptions { + id Int @id @default(autoincrement()) + userId Int @unique + user User @relation(fields: [userId], references: [id]) + + //networks + useNotationColorAsBg Boolean? @default(false) + showNotationMarkerInTableRow Boolean? @default(true) + + //zt central + ztCentralApiKey String? @default("") + ztCentralApiUrl String? @default("https://api.zerotier.com/api/v1") + + // local controller + localControllerUrl String? @default("http://zerotier:9993") + localControllerSecret String? @default("") +} + model User { - id Int @id @default(autoincrement()) - name String - email String @unique - emailVerified DateTime? - lastLogin DateTime - lastseen DateTime? - expirationDate String @default("") - online Boolean? @default(false) - role Role @default(USER) - image String? - hash String - licenseStatus String? - orderStatus String? - orderId Int @default(0) - product_id Int? @default(0) - licenseKey String? @default("") - tempPassword String? - firstTime Boolean @default(true) + id Int @id @default(autoincrement()) + name String + email String @unique + emailVerified DateTime? + lastLogin DateTime + lastseen DateTime? + expirationDate String @default("") + online Boolean? @default(false) + role Role @default(USER) + image String? + hash String + licenseStatus String? + orderStatus String? + orderId Int @default(0) + product_id Int? @default(0) + licenseKey String? @default("") + tempPassword String? + firstTime Boolean @default(true) + + options UserOptions? accounts Account[] sessions Session[] network network[] diff --git a/src/components/layouts/sidebar.tsx b/src/components/layouts/sidebar.tsx index 79b83427..93b381de 100644 --- a/src/components/layouts/sidebar.tsx +++ b/src/components/layouts/sidebar.tsx @@ -15,13 +15,12 @@ import { api } from "~/utils/api"; const Sidebar = (): JSX.Element => { const { open, toggle } = useSidebarStore(); const { data: session } = useSession(); + const { data: me } = api.auth.me.useQuery(); const t = useTranslations("sidebar"); const sidebarRef = useRef(); const router = useRouter(); - const { data: globalOption } = api.admin.getAllOptions.useQuery(); - useEffect(() => { const handleClickOutside = (_event: MouseEvent) => { if (open) { @@ -107,7 +106,7 @@ const Sidebar = (): JSX.Element => { {t("networks")} - {globalOption?.ztCentralApiKey ? ( + {me?.options?.ztCentralApiKey ? (
  • { { enabled: !!nwid }, ); - const { data: options } = api.admin.getAllOptions.useQuery(); + const { data: me } = api.auth.me.useQuery(); const { mutate: updateMemberDatabaseOnly } = api.networkMember.UpdateDatabaseOnly.useMutation(); const { mutate: updateMember } = api.networkMember.Update.useMutation({ @@ -111,7 +111,7 @@ const MemberEditCell = ({ nwid, central = false }: IProp) => {
    {!central && - options?.showNotationMarkerInTableRow && + me?.options?.showNotationMarkerInTableRow && notations?.map((notation) => (
    { }, ); - const { data: options } = api.admin.getAllOptions.useQuery(); + const { data: me } = api.auth.me.useQuery(); useEffect(() => { setData(networkById?.members ?? []); @@ -183,7 +183,7 @@ export const NetworkMembersTable = ({ nwid, central = false }: IProp) => { }`} style={ !central && - options?.useNotationColorAsBg && + me?.options?.useNotationColorAsBg && notation?.length > 0 ? { backgroundColor: convertRGBtoRGBA( diff --git a/src/pages/admin/index.tsx b/src/pages/admin/index.tsx index d2bee2cf..6199239f 100644 --- a/src/pages/admin/index.tsx +++ b/src/pages/admin/index.tsx @@ -6,7 +6,6 @@ import Controller from "./controller"; import Settings from "./settings"; import Mail from "./mail"; import Notification from "./notification"; -import NetworkSettings from "./network"; import { useTranslations } from "next-intl"; import { type GetStaticPropsContext } from "next"; @@ -26,11 +25,6 @@ const AdminSettings = () => { value: "site-setting", component: , }, - { - name: t("tabs.network"), - value: "network-setting", - component: , - }, { name: t("tabs.mail"), value: "mail-setting", diff --git a/src/pages/user-settings/account/index.tsx b/src/pages/user-settings/account/index.tsx index 2d6fb988..3edf9cf5 100644 --- a/src/pages/user-settings/account/index.tsx +++ b/src/pages/user-settings/account/index.tsx @@ -18,18 +18,17 @@ const languageNames = { const Account = () => { const { asPath, locale, locales, push } = useRouter(); const t = useTranslations("userSettings"); + const { data: me, refetch: refetchMe } = api.auth.me.useQuery(); const { data: session, update: sessionUpdate } = useSession(); const { mutate: userUpdate, error: userError } = api.auth.update.useMutation(); - const { mutate: updateZtApi } = api.settings.setZtApi.useMutation({ + const { mutate: updateZtApi } = api.auth.setZtApi.useMutation({ onError: (error) => { toast.error(error.message); }, }); - const { data: globalOption, refetch: refetchOptions } = - api.admin.getAllOptions.useQuery(); const ChangeLanguage = async (locale: string) => { await push(asPath, asPath, { locale }); @@ -38,6 +37,7 @@ const Account = () => { if (userError) { toast.error(userError.message); } + return (
    @@ -172,7 +172,7 @@ const Account = () => { name: "ztCentralApiKey", type: "text", placeholder: "api key", - value: globalOption?.ztCentralApiKey, + value: me?.options?.ztCentralApiKey, }, ]} submitHandler={(params) => { @@ -181,11 +181,11 @@ const Account = () => { { ...params }, { onSuccess: () => { - void refetchOptions(); + void refetchMe(); resolve(true); }, onError: () => { - void refetchOptions(); + void refetchMe(); reject(false); }, }, @@ -208,9 +208,9 @@ const Account = () => { name: "ztCentralApiUrl", type: "text", placeholder: - globalOption?.ztCentralApiUrl || + me?.options?.ztCentralApiUrl || "https://api.zerotier.com/api/v1", - value: globalOption?.ztCentralApiUrl, + value: me?.options?.ztCentralApiUrl, }, ]} submitHandler={(params) => { @@ -219,11 +219,11 @@ const Account = () => { { ...params }, { onSuccess: () => { - void refetchOptions(); + void refetchMe(); resolve(true); }, onError: () => { - void refetchOptions(); + void refetchMe(); reject(false); }, }, diff --git a/src/pages/user-settings/index.tsx b/src/pages/user-settings/index.tsx index 1e5a25ab..79044291 100644 --- a/src/pages/user-settings/index.tsx +++ b/src/pages/user-settings/index.tsx @@ -4,6 +4,7 @@ import { LayoutAuthenticated } from "~/components/layouts/layout"; import Account from "./account"; import { type GetServerSidePropsContext } from "next"; import { useTranslations } from "next-intl"; +import UserNetworkSetting from "./network"; const UserSettings = () => { const router = useRouter(); @@ -22,6 +23,11 @@ const UserSettings = () => { value: "account", component: , }, + { + name: "Network", + value: "network", + component: , + }, ]; const changeTab = async (tab: ITab) => { diff --git a/src/pages/admin/network/index.tsx b/src/pages/user-settings/network/index.tsx similarity index 69% rename from src/pages/admin/network/index.tsx rename to src/pages/user-settings/network/index.tsx index 42d1b044..c747abb5 100644 --- a/src/pages/admin/network/index.tsx +++ b/src/pages/user-settings/network/index.tsx @@ -3,26 +3,11 @@ import { LayoutAuthenticated } from "~/components/layouts/layout"; import { api } from "~/utils/api"; import { useTranslations } from "use-intl"; -const NetworkSetting = () => { +const UserNetworkSetting = () => { const t = useTranslations("admin"); - const { mutate: updateNotation } = - api.admin.updateGlobalNotation.useMutation(); + const { mutate: updateNotation } = api.auth.updateUserNotation.useMutation(); + const { data: me, refetch: refetchMe } = api.auth.me.useQuery(); - const { - data: options, - refetch: refetchOptions, - isLoading: loadingOptions, - } = api.admin.getAllOptions.useQuery(); - - if (loadingOptions) { - return ( -
    -

    - -

    -
    - ); - } return (
    @@ -43,14 +28,14 @@ const NetworkSetting = () => {
    { updateNotation( { showNotationMarkerInTableRow: e.target.checked, }, - { onSuccess: () => void refetchOptions() }, + { onSuccess: () => void refetchMe() }, ); }} /> @@ -68,14 +53,14 @@ const NetworkSetting = () => {
    { updateNotation( { useNotationColorAsBg: e.target.checked, }, - { onSuccess: () => void refetchOptions() }, + { onSuccess: () => void refetchMe() }, ); }} /> @@ -84,8 +69,8 @@ const NetworkSetting = () => {
    ); }; -NetworkSetting.getLayout = function getLayout(page: ReactElement) { +UserNetworkSetting.getLayout = function getLayout(page: ReactElement) { return {page}; }; -export default NetworkSetting; +export default UserNetworkSetting; diff --git a/src/server/api/networkService.ts b/src/server/api/networkService.ts index da4153ae..a44a178f 100644 --- a/src/server/api/networkService.ts +++ b/src/server/api/networkService.ts @@ -8,6 +8,7 @@ import * as ztController from "~/utils/ztApi"; import { prisma } from "../db"; import { MemberEntity, Paths, Peers } from "~/types/local/member"; import { network_members } from "@prisma/client"; +import { UserContext } from "~/types/ctx"; // This function checks if the given IP address is likely a private IP address function isPrivateIP(ip: string): boolean { @@ -115,11 +116,12 @@ export const enrichMembers = async ( }; export const fetchPeersForAllMembers = async ( + ctx: UserContext, members: MemberEntity[], ): Promise => { const memberAddresses = members.map((member) => member.address); const peerPromises = memberAddresses.map((address) => - ztController.peer(address).catch(() => null), + ztController.peer(ctx, address).catch(() => null), ); const peers = await Promise.all(peerPromises); diff --git a/src/server/api/routers/adminRoute.ts b/src/server/api/routers/adminRoute.ts index e0d6fd54..01c6a773 100644 --- a/src/server/api/routers/adminRoute.ts +++ b/src/server/api/routers/adminRoute.ts @@ -49,18 +49,22 @@ export const adminRouter = createTRPCRouter({ return users; }), - getControllerStats: adminRoleProtectedRoute.query(async () => { + getControllerStats: adminRoleProtectedRoute.query(async ({ ctx }) => { const isCentral = false; - const networks = await ztController.get_controller_networks(isCentral); + const networks = await ztController.get_controller_networks(ctx, isCentral); const networkCount = networks.length; let totalMembers = 0; for (const network of networks) { - const members = await ztController.network_members(network as string); + const members = await ztController.network_members( + ctx, + network as string, + ); totalMembers += Object.keys(members).length; } const controllerStatus = (await ztController.get_controller_status( + ctx, isCentral, )) as ZTControllerNodeStatus; return { @@ -307,38 +311,6 @@ export const adminRouter = createTRPCRouter({ } }), - /** - * Update the specified NetworkMemberNotation instance. - * - * This protectedProcedure takes an input of object type with properties: notationId, nodeid, - * useAsTableBackground, and showMarkerInTable. It updates the fields showMarkerInTable and - * useAsTableBackground in the NetworkMemberNotation model for the specified notationId and nodeid. - * - * @input An object with properties: - * - notationId: a number representing the unique ID of the notation - * - nodeid: a number representing the ID of the node to which the notation is linked - * - useAsTableBackground: an optional boolean that determines whether the notation is used as a background in the table - * - showMarkerInTable: an optional boolean that determines whether to show a marker in the table for the notation - * @returns A Promise that resolves with the updated NetworkMemberNotation instance. - */ - updateGlobalNotation: adminRoleProtectedRoute - .input( - z.object({ - useNotationColorAsBg: z.boolean().optional(), - showNotationMarkerInTableRow: z.boolean().optional(), - }), - ) - .mutation(async ({ ctx, input }) => { - return await ctx.prisma.globalOptions.update({ - where: { - id: 1, - }, - data: { - useNotationColorAsBg: input.useNotationColorAsBg, - showNotationMarkerInTableRow: input.showNotationMarkerInTableRow, - }, - }); - }), /** * `unlinkedNetwork` is an admin protected query that fetches and returns detailed information about networks * that are present in the controller but not stored in the database. @@ -355,8 +327,9 @@ export const adminRouter = createTRPCRouter({ * @returns {Promise} - an array of unlinked network details */ unlinkedNetwork: adminRoleProtectedRoute.query(async ({ ctx }) => { - const ztNetworks = - (await ztController.get_controller_networks()) as string[]; + const ztNetworks = (await ztController.get_controller_networks( + ctx, + )) as string[]; const dbNetworks = await ctx.prisma.network.findMany({ select: { nwid: true }, }); @@ -373,7 +346,7 @@ export const adminRouter = createTRPCRouter({ const unlinkArr: NetworkAndMemberResponse[] = await Promise.all( unlinkedNetworks.map((unlinked) => - ztController.local_network_detail(unlinked, false), + ztController.local_network_detail(ctx, unlinked, false), ), ); diff --git a/src/server/api/routers/authRouter.ts b/src/server/api/routers/authRouter.ts index 0baabb40..910c28c6 100644 --- a/src/server/api/routers/authRouter.ts +++ b/src/server/api/routers/authRouter.ts @@ -16,6 +16,7 @@ import { notificationTemplate, } from "~/utils/mail"; import ejs from "ejs"; +import * as ztController from "~/utils/ztApi"; // This regular expression (regex) is used to validate a password based on the following criteria: // - The password must be at least 6 characters long. @@ -122,6 +123,9 @@ export const authRouter = createTRPCRouter({ lastLogin: new Date().toISOString(), role: userCount === 0 ? "ADMIN" : "USER", hash, + options: { + create: {}, // empty object will make Prisma use the default values from the model + }, }, }); @@ -181,10 +185,13 @@ export const authRouter = createTRPCRouter({ }; }), me: protectedProcedure.query(async ({ ctx }) => { - await ctx.prisma.user.findFirst({ + return await ctx.prisma.user.findFirst({ where: { id: ctx.session.user.id, }, + include: { + options: true, + }, }); }), update: protectedProcedure @@ -399,4 +406,95 @@ export const authRouter = createTRPCRouter({ throwError("token is not valid, please try again!"); } }), + /** + * Update the specified NetworkMemberNotation instance. + * + * This protectedProcedure takes an input of object type with properties: notationId, nodeid, + * useAsTableBackground, and showMarkerInTable. It updates the fields showMarkerInTable and + * useAsTableBackground in the NetworkMemberNotation model for the specified notationId and nodeid. + * + * @input An object with properties: + * - notationId: a number representing the unique ID of the notation + * - nodeid: a number representing the ID of the node to which the notation is linked + * - useAsTableBackground: an optional boolean that determines whether the notation is used as a background in the table + * - showMarkerInTable: an optional boolean that determines whether to show a marker in the table for the notation + * @returns A Promise that resolves with the updated NetworkMemberNotation instance. + */ + updateUserNotation: protectedProcedure + .input( + z.object({ + useNotationColorAsBg: z.boolean().optional(), + showNotationMarkerInTableRow: z.boolean().optional(), + }), + ) + .mutation(async ({ ctx, input }) => { + return await ctx.prisma.user.update({ + where: { + id: ctx.session.user.id, + }, + data: { + options: { + upsert: { + create: { + useNotationColorAsBg: input.useNotationColorAsBg, + showNotationMarkerInTableRow: + input.showNotationMarkerInTableRow, + }, + update: { + useNotationColorAsBg: input.useNotationColorAsBg, + showNotationMarkerInTableRow: + input.showNotationMarkerInTableRow, + }, + }, + }, + }, + }); + }), + setZtApi: protectedProcedure + .input( + z.object({ + ztCentralApiKey: z.string().optional(), + ztCentralApiUrl: z.string().optional(), + }), + ) + .mutation(async ({ ctx, input }) => { + // we use upsert in case the user has no options yet + const updated = await ctx.prisma.user.update({ + where: { + id: ctx.session.user.id, + }, + data: { + options: { + upsert: { + create: { + ztCentralApiKey: input.ztCentralApiKey, + ztCentralApiUrl: input.ztCentralApiUrl, + }, + update: { + ztCentralApiKey: input.ztCentralApiKey, + ztCentralApiUrl: input.ztCentralApiUrl, + }, + }, + }, + }, + include: { + options: true, + }, + }); + + if (updated.options?.ztCentralApiKey) { + try { + await ztController.ping_api({ ctx }); + return { status: "success" }; + } catch (error) { + throw new TRPCError({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment + message: error.message, + code: "FORBIDDEN", + }); + } + } + + return updated; + }), }); diff --git a/src/server/api/routers/memberRouter.ts b/src/server/api/routers/memberRouter.ts index 560bec21..c507627c 100644 --- a/src/server/api/routers/memberRouter.ts +++ b/src/server/api/routers/memberRouter.ts @@ -28,6 +28,7 @@ export const networkMemberRouter = createTRPCRouter({ ) .query(async ({ ctx, input }) => { const ztMembers = await ztController.member_details( + ctx, input.nwid, input.id, input.central, @@ -63,6 +64,7 @@ export const networkMemberRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { if (input.central) { return await ztController.member_update({ + ctx, nwid: input.nwid, memberId: input.id, central: input.central, @@ -315,6 +317,7 @@ export const networkMemberRouter = createTRPCRouter({ if (input.central && input?.updateParams?.name) { return await ztController .member_update({ + ctx, nwid: input.nwid, memberId: input.id, central: input.central, @@ -424,6 +427,7 @@ export const networkMemberRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { // remove member from controller const deleted = await ztController.member_delete({ + ctx, central: input.central, nwid: input.nwid, memberId: input.id, diff --git a/src/server/api/routers/networkRouter.ts b/src/server/api/routers/networkRouter.ts index 35c04f97..568d55cc 100644 --- a/src/server/api/routers/networkRouter.ts +++ b/src/server/api/routers/networkRouter.ts @@ -83,7 +83,7 @@ export const networkRouter = createTRPCRouter({ ) .query(async ({ ctx, input }) => { if (input.central) { - return await ztController.get_controller_networks(input.central); + return await ztController.get_controller_networks(ctx, input.central); } const networks = await ctx.prisma.network.findMany({ where: { @@ -110,6 +110,7 @@ export const networkRouter = createTRPCRouter({ .query(async ({ ctx, input }) => { if (input.central) { return await ztController.central_network_detail( + ctx, input.nwid, input.central, ); @@ -130,7 +131,7 @@ export const networkRouter = createTRPCRouter({ return throwError("You are not the Author of this network!"); const ztControllerResponse = await ztController - .local_network_detail(psqlNetworkData.nwid, false) + .local_network_detail(ctx, psqlNetworkData.nwid, false) .catch((err: APIError) => { throwError(`${err.message}`); }); @@ -139,6 +140,7 @@ export const networkRouter = createTRPCRouter({ return throwError("Failed to get network details!"); const peersForAllMembers = await fetchPeersForAllMembers( + ctx, ztControllerResponse.members, ); @@ -214,7 +216,7 @@ export const networkRouter = createTRPCRouter({ try { // Delete ZT network const createCentralNw = await ztController - .network_delete(input.nwid, input.central) + .network_delete(ctx, input.nwid, input.central) .catch(() => []); // Ignore errors if (input.central) return createCentralNw; @@ -246,7 +248,7 @@ export const networkRouter = createTRPCRouter({ }), }), ) - .mutation(async ({ input }) => { + .mutation(async ({ ctx, input }) => { // if central is true, send the request to the central API and return the response const { v4AssignMode } = input.updateParams; // prepare update params @@ -256,6 +258,7 @@ export const networkRouter = createTRPCRouter({ // update network return ztController.network_update({ + ctx, nwid: input.nwid, central: input.central, updateParams, @@ -271,13 +274,14 @@ export const networkRouter = createTRPCRouter({ }), }), ) - .mutation(async ({ input }) => { + .mutation(async ({ ctx, input }) => { const { routes } = input.updateParams; // prepare update params const updateParams = input.central ? { config: { routes } } : { routes }; // update network return ztController.network_update({ + ctx, nwid: input.nwid, central: input.central, updateParams, @@ -293,7 +297,7 @@ export const networkRouter = createTRPCRouter({ }), }), ) - .mutation(async ({ input }) => { + .mutation(async ({ ctx, input }) => { // generate network params const { ipAssignmentPools, routes, v4AssignMode } = IPv4gen( input.updateParams.routes[0].target, @@ -306,6 +310,7 @@ export const networkRouter = createTRPCRouter({ // update network return ztController.network_update({ + ctx, nwid: input.nwid, central: input.central, updateParams, @@ -328,7 +333,7 @@ export const networkRouter = createTRPCRouter({ }), }), ) - .mutation(async ({ input }) => { + .mutation(async ({ ctx, input }) => { const { ipAssignmentPools } = input.updateParams; // prepare update params const updateParams = input.central @@ -337,6 +342,7 @@ export const networkRouter = createTRPCRouter({ // update network return ztController.network_update({ + ctx, nwid: input.nwid, central: input.central, updateParams, @@ -352,13 +358,14 @@ export const networkRouter = createTRPCRouter({ }), }), ) - .mutation(async ({ input }) => { + .mutation(async ({ ctx, input }) => { const updateParams = input.central ? { config: { private: input.updateParams.private } } : { private: input.updateParams.private }; // if central is true, send the request to the central API and return the response const updated = await ztController.network_update({ + ctx, nwid: input.nwid, central: input.central, updateParams, @@ -388,9 +395,9 @@ export const networkRouter = createTRPCRouter({ // if central is true, send the request to the central API and return the response const updated = await ztController.network_update({ + ctx, nwid: input.nwid, central: input.central, - // @ts-expect-error updateParams, }); @@ -423,6 +430,7 @@ export const networkRouter = createTRPCRouter({ // if central is true, send the request to the central API and return the response if (input.central) { const updated = await ztController.network_update({ + ctx, nwid: input.nwid, central: input.central, updateParams: input.updateParams, @@ -476,7 +484,7 @@ export const networkRouter = createTRPCRouter({ .optional(), }), ) - .mutation(async ({ input }) => { + .mutation(async ({ ctx, input }) => { let ztControllerUpdates = {}; // If clearDns is true, set DNS to an empty object @@ -493,6 +501,7 @@ export const networkRouter = createTRPCRouter({ // Send the request to update the network return await ztController.network_update({ + ctx, nwid: input.nwid, central: input.central, updateParams: ztControllerUpdates, @@ -510,16 +519,16 @@ export const networkRouter = createTRPCRouter({ }), }), ) - .mutation(async ({ input }) => { + .mutation(async ({ ctx, input }) => { const updateParams = input.central ? { config: { ...input.updateParams } } : { ...input.updateParams }; try { return await ztController.network_update({ + ctx, nwid: input.nwid, central: input.central, - // @ts-expect-error updateParams, }); } catch (error) { @@ -546,6 +555,7 @@ export const networkRouter = createTRPCRouter({ // Create ZT network const newNw = await ztController.network_create( + ctx, networkName, ipAssignmentPools, input.central, @@ -650,6 +660,7 @@ export const networkRouter = createTRPCRouter({ // update zerotier network with the new flow route const updatedRules = await ztController.network_update({ + ctx, nwid: input.nwid, central: input.central, updateParams, @@ -667,7 +678,7 @@ export const networkRouter = createTRPCRouter({ where: { nwid: input.nwid }, data: { flowRule: flowRoute, - // @ts-expect-error + //@ts-expect-error tagsByName: tags, capabilitiesByName, }, diff --git a/src/server/api/routers/settingsRouter.ts b/src/server/api/routers/settingsRouter.ts index 65e6e2a0..d28ea986 100644 --- a/src/server/api/routers/settingsRouter.ts +++ b/src/server/api/routers/settingsRouter.ts @@ -1,39 +1,12 @@ import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; -import * as ztController from "~/utils/ztApi"; -import { TRPCError } from "@trpc/server"; -import { z } from "zod"; export const settingsRouter = createTRPCRouter({ - setZtApi: protectedProcedure - .input( - z.object({ - ztCentralApiKey: z.string().optional(), - ztCentralApiUrl: z.string().optional(), - }), - ) - .mutation(async ({ ctx, input }) => { - const updated = await ctx.prisma.globalOptions.update({ - where: { - id: 1, - }, - data: { - ...input, - }, - }); - - if (updated.ztCentralApiKey) { - try { - await ztController.ping_api(); - return { status: "success" }; - } catch (error) { - throw new TRPCError({ - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment - message: error.message, - code: "FORBIDDEN", - }); - } - } - - return updated; - }), + // Set global options + getAllOptions: protectedProcedure.query(async ({ ctx }) => { + return await ctx.prisma.globalOptions.findFirst({ + where: { + id: 1, + }, + }); + }), }); diff --git a/src/types/ctx.ts b/src/types/ctx.ts new file mode 100644 index 00000000..4913642d --- /dev/null +++ b/src/types/ctx.ts @@ -0,0 +1,7 @@ +import { PrismaClient } from "@prisma/client"; +import { Session } from "next-auth"; + +export interface UserContext { + session: Session; + prisma: PrismaClient; +} diff --git a/src/types/local/network.d.ts b/src/types/local/network.d.ts index 958d72b9..e147fca4 100644 --- a/src/types/local/network.d.ts +++ b/src/types/local/network.d.ts @@ -13,7 +13,7 @@ export interface NetworkEntity { v6AssignMode: V6AssignMode; authTokens?: null; authorizationEndpoint?: string; - capabilities?: null[] | null; + capabilities?: Capability[]; clientId?: string; creationTime?: number; dns?: dns; @@ -29,6 +29,30 @@ export interface NetworkEntity { cidr?: string[]; tagsByName?: TagsByName; capabilitiesByName?: CapabilitiesByName; + config?: Partial; +} + +interface NetworkConfig { + authTokens?: null; + creationTime?: number; + capabilities?: Capability[]; + enableBroadcast: boolean; + id: string; + ipAssignmentPools: IpAssignmentPool[]; + lastModified: number; + mtu: number; + multicastLimit?: number; + name: string; + private: boolean; + remoteTraceLevel: number; + remoteTraceTarget: string; + routes: Route[]; + rules: Rule[]; + tags: Tag[]; + v4AssignMode: V4AssignMode; + v6AssignMode: V6AssignMode; + dns: { domain?: string; servers?: string[] }; + ssoConfig: { enabled: boolean; mode: string }; } interface CapabilitiesByName { diff --git a/src/types/ztController.ts b/src/types/ztController.ts index 6bbaf0f4..74fd1fbe 100644 --- a/src/types/ztController.ts +++ b/src/types/ztController.ts @@ -1,3 +1,5 @@ +import { UserContext } from "./ctx"; + /* Node status and addressing info https://docs.zerotier.com/service/v1/#operation/getStatus @@ -124,6 +126,7 @@ export interface ZTControllerMemberDetails { export type MemberDeleteResponse = 200 | 401 | 403 | 404; export interface MemberDeleteInput { + ctx: UserContext; nwid: string; memberId: string; central: boolean; diff --git a/src/utils/ztApi.ts b/src/utils/ztApi.ts index 40aeb489..1057978e 100644 --- a/src/utils/ztApi.ts +++ b/src/utils/ztApi.ts @@ -1,5 +1,4 @@ import fs from "fs"; -import { prisma } from "~/server/db"; import { IPv4gen } from "./IPv4gen"; import axios, { type AxiosError, type AxiosResponse } from "axios"; import { APIError } from "~/server/helpers/errorHandler"; @@ -24,6 +23,7 @@ import { import { type MemberEntity } from "~/types/local/member"; import { type NetworkEntity } from "~/types/local/network"; import { type NetworkAndMemberResponse } from "~/types/network"; +import { UserContext } from "~/types/ctx"; const LOCAL_ZT_ADDR = process.env.ZT_ADDR || "http://127.0.0.1:9993"; const CENTRAL_ZT_ADDR = "https://api.zerotier.com/api/v1"; @@ -47,15 +47,43 @@ if (!ZT_SECRET) { } } -const getApiCredentials = async () => { - return await prisma.globalOptions.findFirst({ +const getApiCredentials = async ( + ctx: UserContext, +): Promise<{ + ztCentralApiKey: string | null; + ztCentralApiUrl: string | null; + localControllerSecret: string | null; + localControllerUrl: string | null; +}> => { + const userWithOptions = await ctx.prisma.user.findFirst({ where: { - id: 1, + id: ctx.session.user.id, + }, + select: { + options: { + select: { + ztCentralApiKey: true, + ztCentralApiUrl: true, + localControllerSecret: true, + localControllerUrl: true, + }, + }, }, }); + + // Return only the options. If options are not available, return null values. + return ( + userWithOptions?.options || { + ztCentralApiKey: null, + ztCentralApiUrl: null, + localControllerSecret: null, + localControllerUrl: null, + } + ); }; interface GetOptionsResponse { - ztCentralApiUrl: string | null; + ztCentralApiUrl?: string; + localControllerUrl: string; headers: { Authorization?: string; "X-ZT1-Auth"?: string; @@ -63,11 +91,21 @@ interface GetOptionsResponse { }; } -const getOptions = async (isCentral = false): Promise => { +const getOptions = async ( + ctx: UserContext, + isCentral = false, +): Promise => { + const { + ztCentralApiKey, + ztCentralApiUrl, + localControllerUrl, + localControllerSecret, + } = await getApiCredentials(ctx); + if (isCentral) { - const { ztCentralApiKey, ztCentralApiUrl } = await getApiCredentials(); return { ztCentralApiUrl: ztCentralApiUrl || CENTRAL_ZT_ADDR, + localControllerUrl: null, headers: { Authorization: `token ${ztCentralApiKey}`, "Content-Type": "application/json", @@ -76,9 +114,9 @@ const getOptions = async (isCentral = false): Promise => { } return { - ztCentralApiUrl: null, + localControllerUrl: localControllerUrl || LOCAL_ZT_ADDR, headers: { - "X-ZT1-Auth": ZT_SECRET, + "X-ZT1-Auth": localControllerSecret || ZT_SECRET, "Content-Type": "application/json", }, }; @@ -153,11 +191,13 @@ const postData = async ( /* Controller API for Admin */ - +interface Ictx { + ctx: UserContext; +} //Test API -export const ping_api = async function () { +export const ping_api = async function ({ ctx }: Ictx) { // get headers based on local or central api - const { headers, ztCentralApiUrl } = await getOptions(true); + const { headers, ztCentralApiUrl } = await getOptions(ctx, true); const addr = `${ztCentralApiUrl}/network`; return await getData(addr, headers); @@ -167,11 +207,12 @@ export const ping_api = async function () { // https://docs.zerotier.com/service/v1/#operation/getControllerStatus //Get Version -export const get_controller_version = async function () { - const addr = `${LOCAL_ZT_ADDR}/controller`; - +export const get_controller_version = async function ({ ctx }: Ictx) { // get headers based on local or central api - const { headers } = await getOptions(false); + const { headers, localControllerUrl } = await getOptions(ctx, false); + + const addr = `${localControllerUrl}/controller`; + try { return await getData(addr, headers); } catch (error) { @@ -187,14 +228,18 @@ type ZTControllerListNetworks = Array; // Get all networks export const get_controller_networks = async function ( + ctx: UserContext, isCentral = false, ): Promise { // get headers based on local or central api - const { headers, ztCentralApiUrl } = await getOptions(isCentral); + const { headers, ztCentralApiUrl, localControllerUrl } = await getOptions( + ctx, + isCentral, + ); const addr = isCentral ? `${ztCentralApiUrl}/network` - : `${LOCAL_ZT_ADDR}/controller/network`; + : `${localControllerUrl}/controller/network`; try { if (isCentral) { @@ -216,13 +261,17 @@ export const get_controller_networks = async function ( */ export const get_controller_status = async function ( + ctx: UserContext, isCentral: boolean, ): Promise { - const { headers, ztCentralApiUrl } = await getOptions(isCentral); + const { headers, ztCentralApiUrl, localControllerUrl } = await getOptions( + ctx, + isCentral, + ); const addr = isCentral ? `${ztCentralApiUrl}/status` - : `${LOCAL_ZT_ADDR}/status`; + : `${localControllerUrl}/status`; try { if (isCentral) { @@ -242,12 +291,16 @@ export const get_controller_status = async function ( https://docs.zerotier.com/service/v1/#operation/createNetwork */ export const network_create = async ( + ctx: UserContext, name: string, ipAssignment, isCentral = false, ): Promise => { // get headers based on local or central api - const { headers, ztCentralApiUrl } = await getOptions(isCentral); + const { headers, ztCentralApiUrl, localControllerUrl } = await getOptions( + ctx, + isCentral, + ); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const payload: Partial = { @@ -267,10 +320,11 @@ export const network_create = async ( return flattenNetwork(data); } else { const controllerStatus = (await get_controller_status( + ctx, isCentral, )) as ZTControllerNodeStatus; return await postData( - `${LOCAL_ZT_ADDR}/controller/network/${controllerStatus.address}______`, + `${localControllerUrl}/controller/network/${controllerStatus.address}______`, headers, payload, ); @@ -286,14 +340,18 @@ export const network_create = async ( // https://docs.zerotier.com/service/v1/#operation/deleteNetwork export async function network_delete( + ctx: UserContext, nwid: string, isCentral = false, ): Promise { // get headers based on local or central api - const { headers, ztCentralApiUrl } = await getOptions(isCentral); + const { headers, ztCentralApiUrl, localControllerUrl } = await getOptions( + ctx, + isCentral, + ); const addr = isCentral ? `${ztCentralApiUrl}/network/${nwid}` - : `${LOCAL_ZT_ADDR}/controller/network/${nwid}`; + : `${localControllerUrl}/controller/network/${nwid}`; try { const response = await axios.delete(addr, { headers }); @@ -310,37 +368,43 @@ export async function network_delete( // https://docs.zerotier.com/service/v1/#operation/getControllerNetworkMember export const network_members = async function ( + ctx: UserContext, nwid: string, isCentral = false, ) { // get headers based on local or central api - const { headers, ztCentralApiUrl } = await getOptions(isCentral); + const { headers, ztCentralApiUrl, localControllerUrl } = await getOptions( + ctx, + isCentral, + ); const addr = isCentral ? `${ztCentralApiUrl}/network/${nwid}/member` - : `${LOCAL_ZT_ADDR}/controller/network/${nwid}/member`; + : `${localControllerUrl}/controller/network/${nwid}/member`; // fetch members return await getData(addr, headers); }; export const local_network_detail = async function ( + ctx: UserContext, nwid: string, isCentral = false, ): Promise { // get headers based on local or central api - const { headers } = await getOptions(isCentral); + const { headers, localControllerUrl } = await getOptions(ctx, isCentral); + try { // get all members for a specific network - const members = await network_members(nwid); + const members = await network_members(ctx, nwid); const network = await getData( - `${LOCAL_ZT_ADDR}/controller/network/${nwid}`, + `${localControllerUrl}/controller/network/${nwid}`, headers, ); const membersArr: MemberEntity[] = []; for (const [memberId] of Object.entries(members)) { const memberDetails = await getData( - `${LOCAL_ZT_ADDR}/controller/network/${nwid}/member/${memberId}`, + `${localControllerUrl}/controller/network/${nwid}/member/${memberId}`, headers, ); @@ -359,18 +423,22 @@ export const local_network_detail = async function ( // https://docs.zerotier.com/service/v1/#operation/getNetwork export const central_network_detail = async function ( + ctx: UserContext, nwid: string, isCentral = false, ): Promise { // get headers based on local or central api - const { headers, ztCentralApiUrl } = await getOptions(isCentral); + const { headers, ztCentralApiUrl, localControllerUrl } = await getOptions( + ctx, + isCentral, + ); try { const addr = isCentral ? `${ztCentralApiUrl}/network/${nwid}` - : `${LOCAL_ZT_ADDR}/controller/network/${nwid}`; + : `${localControllerUrl}/controller/network/${nwid}`; // get all members for a specific network - const members = await network_members(nwid, isCentral); + const members = await network_members(ctx, nwid, isCentral); const network = await getData(addr, headers); const membersArr = await Promise.all( @@ -405,6 +473,7 @@ export const central_network_detail = async function ( }; type networkUpdate = { + ctx: UserContext; nwid: string; updateParams: Partial; central?: boolean; @@ -413,15 +482,19 @@ type networkUpdate = { // Get network details // https://docs.zerotier.com/service/v1/#operation/getNetwork export const network_update = async function ({ + ctx, nwid, updateParams: payload, central = false, }: networkUpdate): Promise> { // get headers based on local or central api - const { headers, ztCentralApiUrl } = await getOptions(central); + const { headers, ztCentralApiUrl, localControllerUrl } = await getOptions( + ctx, + central, + ); const addr = central ? `${ztCentralApiUrl}/network/${nwid}` - : `${LOCAL_ZT_ADDR}/controller/network/${nwid}`; + : `${localControllerUrl}/controller/network/${nwid}`; try { return await postData(addr, headers, payload); @@ -436,15 +509,19 @@ export const network_update = async function ({ // https://docs.zerotier.com/service/v1/#operation/deleteControllerNetworkMember export const member_delete = async ({ + ctx, nwid, memberId, central = false, }: MemberDeleteInput): Promise> => { // get headers based on local or central api - const { headers, ztCentralApiUrl } = await getOptions(central); + const { headers, ztCentralApiUrl, localControllerUrl } = await getOptions( + ctx, + central, + ); const addr = central ? `${ztCentralApiUrl}/network/${nwid}/member/${memberId}` - : `${LOCAL_ZT_ADDR}/controller/network/${nwid}/member/${memberId}`; + : `${localControllerUrl}/controller/network/${nwid}/member/${memberId}`; try { const response: AxiosResponse = await axios.delete(addr, { headers }); @@ -457,6 +534,7 @@ export const member_delete = async ({ }; type memberUpdate = { + ctx: UserContext; nwid: string; memberId: string; updateParams: Partial | Partial; @@ -466,16 +544,20 @@ type memberUpdate = { // Update Network Member by ID // https://docs.zerotier.com/service/v1/#operation/updateControllerNetworkMember export const member_update = async ({ + ctx, nwid, memberId, updateParams: payload, central = false, }: memberUpdate) => { // get headers based on local or central api - const { headers, ztCentralApiUrl } = await getOptions(central); + const { headers, ztCentralApiUrl, localControllerUrl } = await getOptions( + ctx, + central, + ); const addr = central ? `${ztCentralApiUrl}/network/${nwid}/member/${memberId}` - : `${LOCAL_ZT_ADDR}/controller/network/${nwid}/member/${memberId}`; + : `${localControllerUrl}/controller/network/${nwid}/member/${memberId}`; try { return await postData(addr, headers, payload); @@ -490,17 +572,21 @@ export const member_update = async ({ // https://docs.zerotier.com/service/v1/#operation/getControllerNetworkMember export const member_details = async function ( + ctx: UserContext, nwid: string, memberId: string, central = false, ): Promise { // get headers based on local or central api - const { headers, ztCentralApiUrl } = await getOptions(central); + const { headers, ztCentralApiUrl, localControllerUrl } = await getOptions( + ctx, + central, + ); try { const addr = central ? `${ztCentralApiUrl}/network/${nwid}/member/${memberId}` - : `${LOCAL_ZT_ADDR}/controller/network/${nwid}/member/${memberId}`; + : `${localControllerUrl}/controller/network/${nwid}/member/${memberId}`; return await getData(addr, headers); } catch (error) { @@ -511,11 +597,14 @@ export const member_details = async function ( // Get all peers // https://docs.zerotier.com/service/v1/#operation/getPeers -export const peers = async (): Promise => { - const addr = `${LOCAL_ZT_ADDR}/peer`; +export const peers = async (ctx: UserContext): Promise => { + // get headers based on local or central api + const { localControllerUrl } = await getOptions(ctx, false); + + const addr = `${localControllerUrl}/peer`; // get headers based on local or central api - const { headers } = await getOptions(false); + const { headers } = await getOptions(ctx, false); try { const response: AxiosResponse = await axios.get(addr, { headers }); @@ -528,11 +617,13 @@ export const peers = async (): Promise => { // Get information about a specific peer by Node ID. // https://docs.zerotier.com/service/v1/#operation/getPeer -export const peer = async (userZtAddress: string) => { - const addr = `${LOCAL_ZT_ADDR}/peer/${userZtAddress}`; +export const peer = async (ctx: UserContext, userZtAddress: string) => { + const { localControllerUrl } = await getOptions(ctx, false); + + const addr = `${localControllerUrl}/peer/${userZtAddress}`; try { // get headers based on local or central api - const { headers } = await getOptions(false); + const { headers } = await getOptions(ctx, false); const response = await getData(addr, headers); if (!response) return {} as ZTControllerGetPeer; From 00f9665212b7a35ef2dfcd29918a9c6512de9cfc Mon Sep 17 00:00:00 2001 From: Bernt Christian Egeland Date: Fri, 11 Aug 2023 12:53:51 +0200 Subject: [PATCH 3/8] option to set local controller url and secret from admin page --- .vscode/extensions.json | 8 +- src/pages/admin/controller/index.tsx | 243 ++++++++++++++++++--------- src/pages/admin/mail/index.tsx | 1 - src/server/api/routers/adminRoute.ts | 87 +++++----- src/server/api/routers/authRouter.ts | 34 ++++ src/utils/ztApi.ts | 2 +- 6 files changed, 255 insertions(+), 120 deletions(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index a0d647d5..7b59e591 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,7 @@ { - "recommendations": ["prisma.prisma"] -} + "recommendations": [ + "prisma.prisma", + "rome.rome", + "bradlc.vscode-tailwindcss" + ] +} \ No newline at end of file diff --git a/src/pages/admin/controller/index.tsx b/src/pages/admin/controller/index.tsx index 47f52ec7..cc777698 100644 --- a/src/pages/admin/controller/index.tsx +++ b/src/pages/admin/controller/index.tsx @@ -1,17 +1,33 @@ import { useTranslations } from "next-intl"; -import { type ReactElement } from "react"; +import { useState, type ReactElement } from "react"; +import EditableField from "~/components/elements/inputField"; import { LayoutAuthenticated } from "~/components/layouts/layout"; import DebugMirror from "~/components/modules/debugController"; import { UnlinkedNetwork } from "~/components/modules/table/unlinkedNetworkTable"; import { api } from "~/utils/api"; const Controller = () => { + const [error, setError] = useState(false); const t = useTranslations("admin"); - const { data: controllerData, isLoading } = - api.admin.getControllerStats.useQuery(); + const { data: controllerData, refetch: refetchStats } = + api.admin.getControllerStats.useQuery(null, { + retry: 1, + onError: () => { + setError(true); + }, + onSuccess: () => { + setError(false); + }, + }); const { data: unlinkedNetworks } = api.admin.unlinkedNetwork.useQuery(); - + const { data: me, refetch: refetchMe } = api.auth.me.useQuery(); + const { mutate: setZtOptions } = api.auth.setLocalZt.useMutation({ + onSuccess: () => { + void refetchMe(); + void refetchStats(); + }, + }); const { networkCount, totalMembers, controllerStatus } = controllerData || {}; const { allowManagementFrom, allowTcpFallbackRelay, listeningOn } = @@ -19,90 +35,161 @@ const Controller = () => { const { online, tcpFallbackActive, version } = controllerStatus || {}; - if (isLoading) { - return ( -
    -

    - -

    -
    - ); - } return (
    -
    -

    - {t("controller.networkMembers.title")} -

    -
    -
    -

    {t("controller.networkMembers.totalNetworks")}

    -

    {networkCount}

    + {error ? ( +
    + + + + Error! Controller unreachable
    -
    -

    {t("controller.networkMembers.totalMembers")}

    -

    {totalMembers}

    -
    - {unlinkedNetworks?.length > 0 ? ( -
    -

    {t("controller.networkMembers.unlinkedNetworks.title")}

    -

    - {t("controller.networkMembers.unlinkedNetworks.description")} + ) : ( + <> +

    +

    + {t("controller.networkMembers.title")}

    - +
    +
    +

    {t("controller.networkMembers.totalNetworks")}

    +

    {networkCount}

    +
    +
    +

    {t("controller.networkMembers.totalMembers")}

    +

    {totalMembers}

    +
    + {unlinkedNetworks && unlinkedNetworks?.length > 0 ? ( +
    +

    {t("controller.networkMembers.unlinkedNetworks.title")}

    +

    + {t("controller.networkMembers.unlinkedNetworks.description")} +

    + +
    + ) : null}
    - ) : null} -
    -
    -

    - {t("controller.management.title")} -

    -
    -
    -

    {t("controller.management.allowManagementFrom")}

    -
    - {allowManagementFrom.map((address) => ( - {address} - ))} +
    +

    + {t("controller.management.title")} +

    +
    +
    +

    {t("controller.management.allowManagementFrom")}

    +
    + {allowManagementFrom?.map((address) => ( + {address} + ))} +
    +
    +
    +

    {t("controller.management.allowTcpFallbackRelay")}

    +

    {allowTcpFallbackRelay ? "Yes" : "No"}

    +
    +
    +

    {t("controller.management.listeningOn")}

    +
    + {listeningOn?.map((address) => ( + {address} + ))} +
    +
    -
    -
    -

    {t("controller.management.allowTcpFallbackRelay")}

    -

    {allowTcpFallbackRelay ? "Yes" : "No"}

    -
    -
    -

    {t("controller.management.listeningOn")}

    -
    - {listeningOn.map((address) => ( - {address} - ))} +
    +

    + {t("controller.controllerStatus.title")} +

    +
    + +
    +

    {t("controller.controllerStatus.online")}

    +

    {online ? "Yes" : "No"}

    +
    +
    +

    {t("controller.controllerStatus.tcpFallbackActive")}

    +

    {tcpFallbackActive ? "Yes" : "No"}

    +
    +
    +

    {t("controller.controllerStatus.version")}

    +

    {version}

    +
    -
    -
    -
    -

    - {t("controller.controllerStatus.title")} -

    -
    +
    +

    Debug

    +
    -
    -

    {t("controller.controllerStatus.online")}

    -

    {online ? "Yes" : "No"}

    -
    -
    -

    {t("controller.controllerStatus.tcpFallbackActive")}

    -

    {tcpFallbackActive ? "Yes" : "No"}

    -
    -
    -

    {t("controller.controllerStatus.version")}

    -

    {version}

    -
    -
    + +
    + + )}
    -

    Debug

    -
    +

    DANGER ZONE

    +
    - +
    +

    + Proceed with Caution: Modifying + the ZeroTier controller URL affects all users. While this offers + flexibility for those wanting a custom controller, be aware of + potential disruptions and compatibility issues. Always backup + configurations before changes. +

    +
    + + new Promise((resolve) => { + setZtOptions(params); + resolve(true); + }) + } + /> +
    +
    + + new Promise((resolve) => { + setZtOptions(params); + resolve(true); + }) + } + /> +
    +
    ); diff --git a/src/pages/admin/mail/index.tsx b/src/pages/admin/mail/index.tsx index 9fc00c2e..bc0d1e8e 100644 --- a/src/pages/admin/mail/index.tsx +++ b/src/pages/admin/mail/index.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import EditableField from "~/components/elements/inputField"; import { type ReactElement } from "react"; import { LayoutAuthenticated } from "~/components/layouts/layout"; diff --git a/src/server/api/routers/adminRoute.ts b/src/server/api/routers/adminRoute.ts index 01c6a773..7cf0730d 100644 --- a/src/server/api/routers/adminRoute.ts +++ b/src/server/api/routers/adminRoute.ts @@ -50,28 +50,35 @@ export const adminRouter = createTRPCRouter({ }), getControllerStats: adminRoleProtectedRoute.query(async ({ ctx }) => { - const isCentral = false; - const networks = await ztController.get_controller_networks(ctx, isCentral); - - const networkCount = networks.length; - let totalMembers = 0; - for (const network of networks) { - const members = await ztController.network_members( + try { + const isCentral = false; + const networks = await ztController.get_controller_networks( ctx, - network as string, + isCentral, ); - totalMembers += Object.keys(members).length; - } - const controllerStatus = (await ztController.get_controller_status( - ctx, - isCentral, - )) as ZTControllerNodeStatus; - return { - networkCount, - totalMembers, - controllerStatus, - }; + const networkCount = networks.length; + let totalMembers = 0; + for (const network of networks) { + const members = await ztController.network_members( + ctx, + network as string, + ); + totalMembers += Object.keys(members).length; + } + + const controllerStatus = (await ztController.get_controller_status( + ctx, + isCentral, + )) as ZTControllerNodeStatus; + return { + networkCount, + totalMembers, + controllerStatus, + }; + } catch (error) { + return throwError(error); + } }), // Set global options @@ -327,30 +334,34 @@ export const adminRouter = createTRPCRouter({ * @returns {Promise} - an array of unlinked network details */ unlinkedNetwork: adminRoleProtectedRoute.query(async ({ ctx }) => { - const ztNetworks = (await ztController.get_controller_networks( - ctx, - )) as string[]; - const dbNetworks = await ctx.prisma.network.findMany({ - select: { nwid: true }, - }); + try { + const ztNetworks = (await ztController.get_controller_networks( + ctx, + )) as string[]; + const dbNetworks = await ctx.prisma.network.findMany({ + select: { nwid: true }, + }); - // create a set of nwid for faster lookup - const dbNetworkIds = new Set(dbNetworks.map((network) => network.nwid)); + // create a set of nwid for faster lookup + const dbNetworkIds = new Set(dbNetworks.map((network) => network.nwid)); - // find networks that are not in database - const unlinkedNetworks = ztNetworks.filter( - (networkId) => !dbNetworkIds.has(networkId), - ); + // find networks that are not in database + const unlinkedNetworks = ztNetworks.filter( + (networkId) => !dbNetworkIds.has(networkId), + ); - if (unlinkedNetworks.length === 0) return []; + if (unlinkedNetworks.length === 0) return []; - const unlinkArr: NetworkAndMemberResponse[] = await Promise.all( - unlinkedNetworks.map((unlinked) => - ztController.local_network_detail(ctx, unlinked, false), - ), - ); + const unlinkArr: NetworkAndMemberResponse[] = await Promise.all( + unlinkedNetworks.map((unlinked) => + ztController.local_network_detail(ctx, unlinked, false), + ), + ); - return unlinkArr; + return unlinkArr; + } catch (_error) { + return throwError("Failed to fetch unlinked networks", _error); + } }), assignNetworkToUser: adminRoleProtectedRoute .input( diff --git a/src/server/api/routers/authRouter.ts b/src/server/api/routers/authRouter.ts index 910c28c6..3b02404a 100644 --- a/src/server/api/routers/authRouter.ts +++ b/src/server/api/routers/authRouter.ts @@ -495,6 +495,40 @@ export const authRouter = createTRPCRouter({ } } + return updated; + }), + setLocalZt: protectedProcedure + .input( + z.object({ + localControllerUrl: z.string().optional(), + localControllerSecret: z.string().optional(), + }), + ) + .mutation(async ({ ctx, input }) => { + // we use upsert in case the user has no options yet + const updated = await ctx.prisma.user.update({ + where: { + id: ctx.session.user.id, + }, + data: { + options: { + upsert: { + create: { + localControllerUrl: input.localControllerUrl, + localControllerSecret: input.localControllerSecret, + }, + update: { + localControllerUrl: input.localControllerUrl, + localControllerSecret: input.localControllerSecret, + }, + }, + }, + }, + include: { + options: true, + }, + }); + return updated; }), }); diff --git a/src/utils/ztApi.ts b/src/utils/ztApi.ts index 1057978e..eac1b64f 100644 --- a/src/utils/ztApi.ts +++ b/src/utils/ztApi.ts @@ -25,7 +25,7 @@ import { type NetworkEntity } from "~/types/local/network"; import { type NetworkAndMemberResponse } from "~/types/network"; import { UserContext } from "~/types/ctx"; -const LOCAL_ZT_ADDR = process.env.ZT_ADDR || "http://127.0.0.1:9993"; +const LOCAL_ZT_ADDR = process.env.ZT_ADDR || "http://zerotier:9993"; const CENTRAL_ZT_ADDR = "https://api.zerotier.com/api/v1"; let ZT_SECRET = process.env.ZT_SECRET; From e7b8844296e3fd5077ba8c7b617efdd83dbb795f Mon Sep 17 00:00:00 2001 From: Bernt Christian Egeland Date: Fri, 11 Aug 2023 13:11:12 +0200 Subject: [PATCH 4/8] translation --- src/components/layouts/sidebar.tsx | 3 +-- src/locales/en/common.json | 22 +++++++++++++------- src/locales/es/common.json | 22 +++++++++++++------- src/locales/no/common.json | 22 +++++++++++++------- src/locales/zh/common.json | 22 +++++++++++++------- src/pages/admin/controller/index.tsx | 25 ++++++++++++++--------- src/pages/user-settings/network/index.tsx | 19 +++++++++-------- 7 files changed, 87 insertions(+), 48 deletions(-) diff --git a/src/components/layouts/sidebar.tsx b/src/components/layouts/sidebar.tsx index 93b381de..f17baae3 100644 --- a/src/components/layouts/sidebar.tsx +++ b/src/components/layouts/sidebar.tsx @@ -290,8 +290,7 @@ const Sidebar = (): JSX.Element => { href="/user-settings?tab=account" className={`flex h-10 flex-row items-center rounded-lg px-3 ${ - router.pathname === "/user-settings" && - router.query.tab === "account" + router.pathname.includes("/user-settings") ? "bg-gray-100 text-gray-700" : "hover:bg-slate-700" }`} diff --git a/src/locales/en/common.json b/src/locales/en/common.json index d86fb013..47c1a967 100644 --- a/src/locales/en/common.json +++ b/src/locales/en/common.json @@ -247,13 +247,6 @@ "authentication": "Authentication", "enableUserRegistration": "Enable user registration?" }, - "networkSetting": { - "memberAnotations": "Member Anotations", - "showMarkerInTable": "Show marker in Table", - "showMarkerInTableDescription": "This will add a circle before the the member name with the anotation color.

    You can still search the anotation if disabled.", - "addBackgroundColorInTable": "Add background color in table", - "addBackgroundColorInTableDescription": "This will add row background color based on the anotation color.

    You can still search the anotation if disabled." - }, "mail": { "mailSMTP": "Mail SMTP", "smtpHost": "SMTP Host", @@ -341,6 +334,14 @@ "tcpFallbackActive": "TCP Fallback Active:", "version": "Version:" }, + "controllerConfig": { + "danger_zone": "DANGER ZONE", + "proceed_with_caution": "Proceed with Caution:", + "modification_warning": "Modifying the ZeroTier controller URL affects all users. While this offers flexibility for those wanting a custom controller, be aware of potential disruptions and compatibility issues. Always backup configurations before changes.", + "local_zerotier_url": "Local Zerotier URL", + "zerotier_secret": "Zerotier Secret", + "submit_empty_field_default": "Submit empty field to use the default." + }, "yes": "Yes", "no": "No" } @@ -363,6 +364,13 @@ "repeatNewPasswordPlaceholder": "Repeat New Password", "role": "Role" }, + "networkSetting": { + "memberAnotations": "Member Anotations", + "showMarkerInTable": "Show marker in Table", + "showMarkerInTableDescription": "This will add a circle before the the member name with the anotation color.

    You can still search the anotation if disabled.", + "addBackgroundColorInTable": "Add background color in table", + "addBackgroundColorInTableDescription": "This will add row background color based on the anotation color.

    You can still search the anotation if disabled." + }, "zerotierCentral": { "title": "Zerotier Central", "description": "Incorporating your ZeroTier Central API will enable you to manage your central networks directly from ZTNET. Upon integrating the key, a new menu will appear in the sidebar for enhanced accessibility and control.

    You can obtain a key from the ZeroTier Central portal. https://my.zerotier.com/account", diff --git a/src/locales/es/common.json b/src/locales/es/common.json index c41dc83f..7470f300 100644 --- a/src/locales/es/common.json +++ b/src/locales/es/common.json @@ -246,13 +246,6 @@ "authentication": "Autenticación", "enableUserRegistration": "¿Habilitar registro de usuario?" }, - "networkSetting": { - "memberAnotations": "Anotaciones de miembro", - "showMarkerInTable": "Mostrar marcador en la tabla", - "showMarkerInTableDescription": "Esto agregará un círculo antes del nombre del miembro con el color de la anotación.

    Todavía puedes buscar la anotación si está desactivada.", - "addBackgroundColorInTable": "Agregar color de fondo en la tabla", - "addBackgroundColorInTableDescription": "Esto agregará color de fondo a la fila según el color de la anotación.

    Todavía puedes buscar la anotación si está desactivada." - }, "mail": { "mailSMTP": "Mail SMTP", "smtpHost": "Host SMTP", @@ -341,6 +334,14 @@ "tcpFallbackActive": "TCP Fallback activo:", "version": "Versión:" }, + "controllerConfig": { + "danger_zone": "ZONA DE PELIGRO", + "proceed_with_caution": "Proceda con precaución:", + "modification_warning": "Modificar la URL del controlador ZeroTier afecta a todos los usuarios. Aunque esto ofrece flexibilidad para aquellos que quieren un controlador personalizado, ten en cuenta las posibles interrupciones y problemas de compatibilidad. Siempre realiza una copia de seguridad de las configuraciones antes de hacer cambios.", + "local_zerotier_url": "URL local de Zerotier", + "zerotier_secret": "Secreto de Zerotier", + "submit_empty_field_default": "Deje el campo vacío para usar el valor predeterminado." + }, "yes": "Sí", "no": "No" } @@ -363,6 +364,13 @@ "repeatNewPasswordPlaceholder": "Repetir Nueva Contraseña", "role": "Rol" }, + "networkSetting": { + "memberAnotations": "Anotaciones de miembro", + "showMarkerInTable": "Mostrar marcador en la tabla", + "showMarkerInTableDescription": "Esto agregará un círculo antes del nombre del miembro con el color de la anotación.

    Todavía puedes buscar la anotación si está desactivada.", + "addBackgroundColorInTable": "Agregar color de fondo en la tabla", + "addBackgroundColorInTableDescription": "Esto agregará color de fondo a la fila según el color de la anotación.

    Todavía puedes buscar la anotación si está desactivada." + }, "zerotierCentral": { "title": "Zerotier Central", "description": "Incorporar su API central de ZeroTier le permitirá gestionar sus redes centrales directamente desde ZTNET. Al integrar la clave, aparecerá un nuevo menú en la barra lateral para mejorar la accesibilidad y el control.

    Puede obtener una clave desde el portal central de ZeroTier. https://my.zerotier.com/account", diff --git a/src/locales/no/common.json b/src/locales/no/common.json index e3128c94..84e11c37 100644 --- a/src/locales/no/common.json +++ b/src/locales/no/common.json @@ -247,13 +247,6 @@ "authentication": "Autentisering", "enableUserRegistration": "Aktiver brukerregistrering?" }, - "networkSetting": { - "memberAnotations": "Medlemsanmerkninger", - "showMarkerInTable": "Vis markør i tabellen", - "showMarkerInTableDescription": "Dette vil legge til en sirkel før medlemsnavnet med anmerkningsfargen.

    Du kan fremdeles søke anmerkningen hvis deaktivert.", - "addBackgroundColorInTable": "Legg til bakgrunnsfarge i tabellen", - "addBackgroundColorInTableDescription": "Dette vil legge til radbakgrunnsfarge basert på anmerkningsfargen.

    Du kan fremdeles søke anmerkningen hvis deaktivert." - }, "mail": { "mailSMTP": "Mail SMTP", "smtpHost": "SMTP-vert", @@ -341,6 +334,14 @@ "tcpFallbackActive": "TCP Fallback er aktiv:", "version": "Versjon:" }, + "controllerConfig": { + "danger_zone": "FARESONE", + "proceed_with_caution": "Fortsett med forsiktighet:", + "modification_warning": "Å endre ZeroTier-kontrollerens URL påvirker alle brukere. Selv om dette gir fleksibilitet for de som ønsker en tilpasset kontroller, vær oppmerksom på potensielle avbrudd og kompatibilitetsproblemer. Ta alltid sikkerhetskopi av konfigurasjoner før endringer.", + "local_zerotier_url": "Lokal Zerotier URL", + "zerotier_secret": "Zerotier Hemmelighet", + "submit_empty_field_default": "Send inn tomt felt for å bruke standardinnstillingen." + }, "yes": "Ja", "no": "Nei" } @@ -363,6 +364,13 @@ "repeatNewPasswordPlaceholder": "Gjenta Nytt Passord", "role": "Rolle" }, + "networkSetting": { + "memberAnotations": "Medlemsanmerkninger", + "showMarkerInTable": "Vis markør i tabellen", + "showMarkerInTableDescription": "Dette vil legge til en sirkel før medlemsnavnet med anmerkningsfargen.

    Du kan fremdeles søke anmerkningen hvis deaktivert.", + "addBackgroundColorInTable": "Legg til bakgrunnsfarge i tabellen", + "addBackgroundColorInTableDescription": "Dette vil legge til radbakgrunnsfarge basert på anmerkningsfargen.

    Du kan fremdeles søke anmerkningen hvis deaktivert." + }, "zerotierCentral": { "title": "Zerotier Central", "description": "Ved å integrere din ZeroTier Central API vil du kunne administrere dine sentrale nettverk direkte fra ZTNET. Når du integrerer nøkkelen, vil en ny meny dukke opp i sidemenyen for økt tilgjengelighet og kontroll.

    Du kan skaffe en nøkkel fra ZeroTier Central portal. https://my.zerotier.com/account", diff --git a/src/locales/zh/common.json b/src/locales/zh/common.json index 46f66994..f7efccab 100644 --- a/src/locales/zh/common.json +++ b/src/locales/zh/common.json @@ -247,13 +247,6 @@ "authentication": "身份验证", "enableUserRegistration": "启用用户注册?" }, - "networkSetting": { - "memberAnotations": "成员注释", - "showMarkerInTable": "在表格中显示标记", - "showMarkerInTableDescription": "这将在成员名称前添加一个带有注释颜色的圆圈。

    如果禁用,您仍然可以搜索注释。", - "addBackgroundColorInTable": "在表格中添加背景颜色", - "addBackgroundColorInTableDescription": "这将根据注释颜色添加行背景颜色。

    如果禁用,您仍然可以搜索注释。" - }, "mail": { "mailSMTP": "邮件SMTP", "smtpHost": "SMTP主机", @@ -341,6 +334,14 @@ "tcpFallbackActive": "TCP Fallback 激活:", "version": "版本:" }, + "controllerConfig": { + "danger_zone": "危险区域", + "proceed_with_caution": "请谨慎操作:", + "modification_warning": "修改ZeroTier控制器URL会影响所有用户。虽然这为想要自定义控制器的人提供了灵活性,但请注意可能的中断和兼容性问题。更改前始终备份配置。", + "local_zerotier_url": "本地Zerotier URL", + "zerotier_secret": "Zerotier密码", + "submit_empty_field_default": "提交空字段以使用默认值。" + }, "yes": "是", "no": "否" } @@ -363,6 +364,13 @@ "repeatNewPasswordPlaceholder": "重复新密码", "role": "角色" }, + "networkSetting": { + "memberAnotations": "成员注释", + "showMarkerInTable": "在表格中显示标记", + "showMarkerInTableDescription": "这将在成员名称前添加一个带有注释颜色的圆圈。

    如果禁用,您仍然可以搜索注释。", + "addBackgroundColorInTable": "在表格中添加背景颜色", + "addBackgroundColorInTableDescription": "这将根据注释颜色添加行背景颜色。

    如果禁用,您仍然可以搜索注释。" + }, "zerotierCentral": { "title": "Zerotier Central", "description": "整合您的ZeroTier Central API将使您能够直接从ZTNET管理您的中央网络。集成密钥后,侧边栏中将出现一个新菜单,以增强可访问性和控制。

    您可以从ZeroTier Central门户获取密钥。https://my.zerotier.com/account", diff --git a/src/pages/admin/controller/index.tsx b/src/pages/admin/controller/index.tsx index cc777698..34b30a6c 100644 --- a/src/pages/admin/controller/index.tsx +++ b/src/pages/admin/controller/index.tsx @@ -133,22 +133,25 @@ const Controller = () => { )}
    -

    DANGER ZONE

    +

    + {t("controller.controllerConfig.danger_zone")} +

    - Proceed with Caution: Modifying - the ZeroTier controller URL affects all users. While this offers - flexibility for those wanting a custom controller, be aware of - potential disruptions and compatibility issues. Always backup - configurations before changes. + {t.rich("controller.controllerConfig.proceed_with_caution", { + span: (content) => {content} , + })} + {t("controller.controllerConfig.modification_warning")}

    {
    { - const t = useTranslations("admin"); + const t = useTranslations("userSettings"); const { mutate: updateNotation } = api.auth.updateUserNotation.useMutation(); const { data: me, refetch: refetchMe } = api.auth.me.useQuery(); @@ -12,16 +12,16 @@ const UserNetworkSetting = () => {

    - {t("networkSetting.memberAnotations")} + {t("account.networkSetting.memberAnotations")}

    - {t("networkSetting.showMarkerInTable")} + {t("account.networkSetting.showMarkerInTable")}

    - {t.rich("networkSetting.showMarkerInTableDescription", { + {t.rich("account.networkSetting.showMarkerInTableDescription", { br: () =>
    , })}

    @@ -43,12 +43,15 @@ const UserNetworkSetting = () => {

    - {t("networkSetting.addBackgroundColorInTable")} + {t("account.networkSetting.addBackgroundColorInTable")}

    - {t.rich("networkSetting.addBackgroundColorInTableDescription", { - br: () =>
    , - })} + {t.rich( + "account.networkSetting.addBackgroundColorInTableDescription", + { + br: () =>
    , + }, + )}

    Date: Fri, 11 Aug 2023 22:48:29 +0200 Subject: [PATCH 5/8] timeago format, icon, shorten table header name --- src/components/layouts/sidebar.tsx | 16 ++++++---- .../modules/table/memberHeaderColumns.tsx | 30 +++++++++++-------- .../modules/table/networkMembersTable.tsx | 1 - src/locales/en/common.json | 4 +-- src/locales/es/common.json | 8 ++--- src/locales/no/common.json | 4 +-- src/locales/zh/common.json | 2 +- src/server/api/routers/memberRouter.ts | 6 ++-- 8 files changed, 42 insertions(+), 29 deletions(-) diff --git a/src/components/layouts/sidebar.tsx b/src/components/layouts/sidebar.tsx index f17baae3..bcab3718 100644 --- a/src/components/layouts/sidebar.tsx +++ b/src/components/layouts/sidebar.tsx @@ -64,6 +64,7 @@ const Sidebar = (): JSX.Element => { }`} > + {/* https://heroicons.com/ */} { }`} > + {/* https://heroicons.com/ */} { : "hover:bg-slate-700" }`} > + {/* https://heroicons.com/ */} - + diff --git a/src/components/modules/table/memberHeaderColumns.tsx b/src/components/modules/table/memberHeaderColumns.tsx index 49c8ad0b..c56e93c7 100644 --- a/src/components/modules/table/memberHeaderColumns.tsx +++ b/src/components/modules/table/memberHeaderColumns.tsx @@ -138,10 +138,13 @@ export const MemberHeaderColumns = ({ nwid, central = false }: IProp) => { const formatTime = (value: string, unit: string) => { // Map full unit names to their abbreviations const unitAbbreviations: { [key: string]: string } = { - second: "s ago", - minute: "m ago", - hour: "hr ago", - day: "dy ago", + second: "sec ago", + minute: "min ago", + hour: "hours ago", + day: "days ago", + week: "weeks ago", + month: "months ago", + year: "years ago", }; const abbreviation = unitAbbreviations[unit] || unit; @@ -197,18 +200,21 @@ export const MemberHeaderColumns = ({ nwid, central = false }: IProp) => { const unitAbbreviations: { [key: string]: string } = { second: "sec", minute: "min", - hour: "hr", - day: "d", + hour: "hour", + day: "day", + week: "week", + month: "month", + year: "year", }; const abbreviation = unitAbbreviations[unit] || unit; return `${value} ${abbreviation}`; }; const cursorStyle = { cursor: "pointer" }; - // console.log(original); + if (central) { - const now = Date.now(); // current timestamp in milliseconds const lastSeen = original?.lastSeen; // assuming lastSeen is a timestamp in milliseconds + const now = Date.now(); // current timestamp in milliseconds const fiveMinutesAgo = now - 5 * 60 * 1000; // timestamp 5 minutes ago // Check if lastSeen is within the last 5 minutes @@ -270,15 +276,15 @@ export const MemberHeaderColumns = ({ nwid, central = false }: IProp) => { ? ` (v${original.peers.version})` : ""; return ( - {t("networkById.networkMembersTable.column.conStatus.direct", { version: versionInfo, - })} - + })}{" "} +
    ); } diff --git a/src/components/modules/table/networkMembersTable.tsx b/src/components/modules/table/networkMembersTable.tsx index 375a36e8..df13b8ca 100644 --- a/src/components/modules/table/networkMembersTable.tsx +++ b/src/components/modules/table/networkMembersTable.tsx @@ -200,7 +200,6 @@ export const NetworkMembersTable = ({ nwid, central = false }: IProp) => { .getVisibleCells() .map((cell) => ( // Apply the cell props - { // Render the cell contents diff --git a/src/locales/en/common.json b/src/locales/en/common.json index 47c1a967..58b8fdbc 100644 --- a/src/locales/en/common.json +++ b/src/locales/en/common.json @@ -127,7 +127,7 @@ "networkMembersTable": { "searchPlaceholder": "Search anything...", "column": { - "authorized": "Authorized", + "authorized": "Auth", "name": "Name", "id": "ID", "ipAssignments": { @@ -140,7 +140,7 @@ "unknownValue": "Unknown" }, "conStatus": { - "header": "Conn Status", + "header": "Status", "toolTip": "Could not establish direct connection and is currently being Relayed through zerotier servers with higher latency", "relayed": "RELAYED", "directLan": "Direct LAN connection established", diff --git a/src/locales/es/common.json b/src/locales/es/common.json index 7470f300..e27a8dfa 100644 --- a/src/locales/es/common.json +++ b/src/locales/es/common.json @@ -23,7 +23,8 @@ "loading": "Cargando", "title": "Redes", "description": "Redes VPN", - "addNetworkButton": "Crear una red" + "addNetworkButton": "Crear una red", + "noNetworksMessage": "No se han creado redes todavía. Comienza creando una y aparecerá aquí." }, "networksTable": { "name": "Nombre", @@ -126,7 +127,7 @@ "networkMembersTable": { "searchPlaceholder": "Buscar cualquier cosa...", "column": { - "authorized": "Autorizado", + "authorized": "Auth", "name": "Nombre", "id": "ID", "ipAssignments": { @@ -139,7 +140,7 @@ "unknownValue": "Desconocido" }, "conStatus": { - "header": "Estado de la conexión", + "header": "Estado", "toolTip": "No se pudo establecer una conexión directa y actualmente se está retransmitiendo a través de los servidores zerotier con mayor latencia", "relayed": "REENVIADO", "directLan": "Conexión directa LAN establecida", @@ -301,7 +302,6 @@ "allAdminsNotification": "Todos los administradores recibirán notificaciones" }, "controller": { - "loading": "Cargando...", "networkMembers": { "title": "Redes & Miembros", "totalNetworks": "Total de redes:", diff --git a/src/locales/no/common.json b/src/locales/no/common.json index 84e11c37..ed05d377 100644 --- a/src/locales/no/common.json +++ b/src/locales/no/common.json @@ -127,7 +127,7 @@ "networkMembersTable": { "searchPlaceholder": "Søk etter noe...", "column": { - "authorized": "Autorisert", + "authorized": "Auth", "name": "Navn", "id": "ID", "ipAssignments": { @@ -140,7 +140,7 @@ "unknownValue": "Ukjent" }, "conStatus": { - "header": "Tilkoblingsstatus", + "header": "Status", "toolTip": "Kunne ikke opprette direkte tilkobling og blir for øyeblikket formidlet gjennom zerotier-servere med høyere latenstid", "relayed": "VIDEREFORMIDLET", "directLan": "Direkte LAN-tilkobling opprettet", diff --git a/src/locales/zh/common.json b/src/locales/zh/common.json index f7efccab..de2e50ef 100644 --- a/src/locales/zh/common.json +++ b/src/locales/zh/common.json @@ -127,7 +127,7 @@ "networkMembersTable": { "searchPlaceholder": "搜索任何东西...", "column": { - "authorized": "已授权", + "authorized": "授权", "name": "名字", "id": "ID", "ipAssignments": { diff --git a/src/server/api/routers/memberRouter.ts b/src/server/api/routers/memberRouter.ts index c507627c..56c28ce4 100644 --- a/src/server/api/routers/memberRouter.ts +++ b/src/server/api/routers/memberRouter.ts @@ -130,7 +130,7 @@ export const networkMemberRouter = createTRPCRouter({ }), }), ) - .mutation(async ({ input }) => { + .mutation(async ({ ctx, input }) => { const payload: Partial = {}; // update capabilities @@ -193,6 +193,7 @@ export const networkMemberRouter = createTRPCRouter({ // if central is true, send the request to the central API and return the response const updatedMember = await ztController .member_update({ + ctx, nwid: input.nwid, memberId: input.memberId, central: input.central, @@ -267,7 +268,7 @@ export const networkMemberRouter = createTRPCRouter({ }), }), ) - .mutation(async ({ input }) => { + .mutation(async ({ ctx, input }) => { const tags = input.updateParams.tags; const adjustedTags = tags && tags.length === 0 ? [] : tags; @@ -281,6 +282,7 @@ export const networkMemberRouter = createTRPCRouter({ // if central is true, send the request to the central API and return the response const updatedMember = await ztController .member_update({ + ctx, nwid: input.nwid, memberId: input.memberId, central: input.central, From f2e2e0a32fd6e500792ba0212db3603fe72eaec5 Mon Sep 17 00:00:00 2001 From: Bernt Christian Egeland Date: Fri, 11 Aug 2023 23:00:53 +0200 Subject: [PATCH 6/8] tests --- src/__tests__/__mocks__/networkById.ts | 27 ++++++++++++++++--- .../modules/deletedNetworkMembersTable.tsx | 1 - 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/__tests__/__mocks__/networkById.ts b/src/__tests__/__mocks__/networkById.ts index a19a2536..ae2c2349 100644 --- a/src/__tests__/__mocks__/networkById.ts +++ b/src/__tests__/__mocks__/networkById.ts @@ -2,6 +2,25 @@ import "@testing-library/jest-dom"; jest.mock("../../utils/api", () => ({ api: { + auth: { + me: { + useQuery: () => ({ + data: { + user: { + id: "1234567890", + email: "", + role: "ADMIN", + verified: true, + avatar: null, + createdAt: "2021-05-04T12:00:00.000Z", + updatedAt: "2021-05-04T12:00:00.000Z", + }, + }, + isLoading: false, + refetch: jest.fn(), + }), + }, + }, admin: { getAllOptions: { useQuery: () => ({ @@ -53,7 +72,7 @@ jest.mock("../../utils/api", () => ({ useMutation: () => ({ mutate: jest.fn(), }), - } + }, }, network: { getNetworkById: { @@ -88,8 +107,8 @@ jest.mock("../../utils/api", () => ({ }, ], v4AssignMode: { - zt: false, // Changed state to checked - }, + zt: false, // Changed state to checked + }, }, members: [], zombieMembers: [], @@ -170,4 +189,4 @@ jest.mock("../../utils/api", () => ({ }, }, }, -})); \ No newline at end of file +})); diff --git a/src/components/modules/deletedNetworkMembersTable.tsx b/src/components/modules/deletedNetworkMembersTable.tsx index f8379492..6e628a20 100644 --- a/src/components/modules/deletedNetworkMembersTable.tsx +++ b/src/components/modules/deletedNetworkMembersTable.tsx @@ -45,7 +45,6 @@ export const DeletedNetworkMembersTable = ({ nwid }) => { }, }); const columnHelper = createColumnHelper(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any const columns = useMemo[]>( () => [ columnHelper.accessor("authorized", { From ca4612ddf13d4026443b95299dabb072960b8f7f Mon Sep 17 00:00:00 2001 From: Bernt Christian Egeland Date: Fri, 11 Aug 2023 23:29:49 +0200 Subject: [PATCH 7/8] seed db with user options --- init-db.sh | 5 +++++ package-lock.json | 8 ++++---- package.json | 5 ++++- prisma/seed.ts | 19 +++++++++++++++++++ prisma/user-option.seed.ts | 32 ++++++++++++++++++++++++++++++++ 5 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 prisma/seed.ts create mode 100644 prisma/user-option.seed.ts diff --git a/init-db.sh b/init-db.sh index 3583f37a..2f9fa0f9 100644 --- a/init-db.sh +++ b/init-db.sh @@ -49,5 +49,10 @@ echo "Applying migrations to the database..." npx prisma migrate deploy echo "Migrations applied successfully!" +# seed the database +echo "Seeding the database..." +npx prisma db seed +echo "Database seeded successfully!" + >&2 echo "Executing command" exec $cmd \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 71cb147b..64d5caae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,7 +58,7 @@ "@types/ejs": "^3.1.2", "@types/jest": "^29.5.0", "@types/jsonwebtoken": "^9.0.2", - "@types/node": "^18.14.0", + "@types/node": "^18.17.5", "@types/nodemailer": "^6.4.8", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", @@ -2476,9 +2476,9 @@ } }, "node_modules/@types/node": { - "version": "18.17.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.4.tgz", - "integrity": "sha512-ATL4WLgr7/W40+Sp1WnNTSKbgVn6Pvhc/2RHAdt8fl6NsQyp4oPCi2eKcGOvA494bwf1K/W6nGgZ9TwDqvpjdw==", + "version": "18.17.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.5.tgz", + "integrity": "sha512-xNbS75FxH6P4UXTPUJp/zNPq6/xsfdJKussCWNOnz4aULWIRwMgP1LgaB5RiBnMX1DPCYenuqGZfnIAx5mbFLA==", "devOptional": true }, "node_modules/@types/nodemailer": { diff --git a/package.json b/package.json index c9f86b96..b0fe283a 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@types/ejs": "^3.1.2", "@types/jest": "^29.5.0", "@types/jsonwebtoken": "^9.0.2", - "@types/node": "^18.14.0", + "@types/node": "^18.17.5", "@types/nodemailer": "^6.4.8", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", @@ -87,5 +87,8 @@ }, "ct3aMetadata": { "initVersion": "7.8.0" + }, + "prisma": { + "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts" } } diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 00000000..79f91681 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,19 @@ +import { seedUserOptions } from "./user-option.seed"; +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +async function main() { + await seedUserOptions(); + // rome-ignore lint/nursery/noConsoleLog: + console.log("Seeding User Options complete!"); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/prisma/user-option.seed.ts b/prisma/user-option.seed.ts new file mode 100644 index 00000000..54b7c620 --- /dev/null +++ b/prisma/user-option.seed.ts @@ -0,0 +1,32 @@ +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +export async function seedUserOptions() { + // Fetch all users from the database + const users = await prisma.user.findMany(); + + for (const user of users) { + // Check if UserOptions exist for each user + const userOptionExists = await prisma.userOptions.findUnique({ + where: { userId: user.id }, + }); + // If UserOptions do not exist for a user, create them + if (!userOptionExists) { + await prisma.userOptions.create({ + data: { + userId: user.id, + useNotationColorAsBg: false, + showNotationMarkerInTableRow: true, + ztCentralApiKey: "", + ztCentralApiUrl: "https://api.zerotier.com/api/v1", + localControllerUrl: "http://zerotier:9993", + localControllerSecret: "", + }, + }); + } + } + + // rome-ignore lint/nursery/noConsoleLog: + console.log("Seeding User Options complete!"); +} From 5b2ed0edbb5a3ba769edafb99268f20af2514041 Mon Sep 17 00:00:00 2001 From: Bernt Christian Egeland Date: Fri, 11 Aug 2023 23:43:42 +0200 Subject: [PATCH 8/8] moved seed to its own folder --- prisma/seed.ts | 2 +- prisma/{ => seeds}/user-option.seed.ts | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename prisma/{ => seeds}/user-option.seed.ts (100%) diff --git a/prisma/seed.ts b/prisma/seed.ts index 79f91681..4af35463 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,4 +1,4 @@ -import { seedUserOptions } from "./user-option.seed"; +import { seedUserOptions } from "./seeds/user-option.seed"; import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient(); diff --git a/prisma/user-option.seed.ts b/prisma/seeds/user-option.seed.ts similarity index 100% rename from prisma/user-option.seed.ts rename to prisma/seeds/user-option.seed.ts