diff --git a/prisma/migrations/20240803114906_global_node_naming/migration.sql b/prisma/migrations/20240803114906_global_node_naming/migration.sql new file mode 100644 index 00000000..614c85de --- /dev/null +++ b/prisma/migrations/20240803114906_global_node_naming/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "OrganizationSettings" ADD COLUMN "renameNodeGlobally" BOOLEAN DEFAULT false; + +-- AlterTable +ALTER TABLE "UserOptions" ADD COLUMN "renameNodeGlobally" BOOLEAN DEFAULT false; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 963128ec..6bc638a3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -182,6 +182,7 @@ model UserOptions { // member table deAuthorizeWarning Boolean? @default(false) addMemberIdAsName Boolean? @default(false) + renameNodeGlobally Boolean? @default(false) } enum AccessLevel { @@ -275,6 +276,9 @@ model Invitation { organizations OrganizationInvitation[] } +// +// ORGANIZATION +// model OrganizationInvitation { id String @id @default(cuid()) invitationId Int @@ -283,10 +287,6 @@ model OrganizationInvitation { organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) } -// -// ORGANIZATION -// - model Organization { id String @id @default(cuid()) createdAt DateTime @default(now()) @@ -359,10 +359,10 @@ model LastReadMessage { } model OrganizationSettings { - id Int @id @default(autoincrement()) - organizationId String @unique - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - // Add specific settings fields here + id Int @id @default(autoincrement()) + renameNodeGlobally Boolean? @default(false) + organizationId String @unique + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) } model MembershipRequest { @@ -379,7 +379,7 @@ model ActivityLog { createdAt DateTime @default(now()) performedById String performedBy User @relation(fields: [performedById], references: [id], onDelete: Cascade) - organizationId String? // Make this optional + organizationId String? organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade) } diff --git a/src/pages/user-settings/network/index.tsx b/src/pages/user-settings/network/index.tsx index efd5c9cf..c1797644 100644 --- a/src/pages/user-settings/network/index.tsx +++ b/src/pages/user-settings/network/index.tsx @@ -77,6 +77,45 @@ const UserNetworkSetting = () => { {t("network.memberTable.memberTableTitle")}

+
+
+

Enable global node naming

+

+ When enabled, this feature will: +

+

+ Note: This feature has priority over "Add Member ID as Name". It applies + only to networks where you are the author and doesn't affect networks + managed by others or organizations. +

+

+
+ { + updateSettings( + { + renameNodeGlobally: e.target.checked, + }, + { onSuccess: () => void refetchMe() }, + ); + }} + /> +

diff --git a/src/server/api/routers/authRouter.ts b/src/server/api/routers/authRouter.ts index 9767d13f..bb4d0cd1 100644 --- a/src/server/api/routers/authRouter.ts +++ b/src/server/api/routers/authRouter.ts @@ -619,6 +619,7 @@ export const authRouter = createTRPCRouter({ showNotationMarkerInTableRow: z.boolean().optional(), deAuthorizeWarning: z.boolean().optional(), addMemberIdAsName: z.boolean().optional(), + renameNodeGlobally: z.boolean().optional(), }), ) .mutation(async ({ ctx, input }) => { diff --git a/src/server/api/routers/memberRouter.ts b/src/server/api/routers/memberRouter.ts index 5d603ebc..587ca946 100644 --- a/src/server/api/routers/memberRouter.ts +++ b/src/server/api/routers/memberRouter.ts @@ -392,6 +392,74 @@ export const networkMemberRouter = createTRPCRouter({ }); } + // get the user options + const userOptions = await ctx.prisma.userOptions.findFirst({ + where: { + userId: ctx.session.user.id, + }, + }); + + // check if the user wants to update the name globally + const shouldUpdateNameGlobally = userOptions?.renameNodeGlobally || false; + if (shouldUpdateNameGlobally && input.updateParams.name && !input.organizationId) { + // Find all networks where the user is the author + const userNetworks = await ctx.prisma.network.findMany({ + where: { + authorId: ctx.session.user.id, + organizationId: null, // Only private networks + }, + select: { nwid: true }, + }); + + // Update the node name across all user's private networks + await ctx.prisma.network_members.updateMany({ + where: { + id: input.id, + nwid: { in: userNetworks.map((network) => network.nwid) }, + }, + data: { + name: input.updateParams.name, + }, + }); + } + // Check if the organization wants to update the node name globally + if (input.organizationId && input.updateParams.name) { + // Upsert OrganizationSettings to ensure it exists + const organizationOptions = await ctx.prisma.organizationSettings.upsert({ + where: { organizationId: input.organizationId }, + update: {}, + create: { organizationId: input.organizationId }, + }); + + // Check if the organization wants to update the name globally + if (organizationOptions.renameNodeGlobally && input.updateParams.name) { + // Update node name across all organization networks in a single query + await ctx.prisma.network_members.updateMany({ + where: { + id: input.id, + nwid_ref: { + organizationId: input.organizationId, + }, + }, + data: { + name: input.updateParams.name, + }, + }); + } else if (input.updateParams.name) { + // Update only the specific network member if global renaming is off + await ctx.prisma.network_members.updateMany({ + where: { + id: input.id, + nwid_ref: { + organizationId: input.organizationId, + }, + }, + data: { + name: input.updateParams.name, + }, + }); + } + } // if users click the re-generate icon on IP address const response = await ctx.prisma.network.update({ where: { diff --git a/src/server/api/services/memberService.ts b/src/server/api/services/memberService.ts index 7ecbe94d..c0f6ba65 100644 --- a/src/server/api/services/memberService.ts +++ b/src/server/api/services/memberService.ts @@ -120,35 +120,49 @@ const findActivePreferredPeerPath = (peers: Peers | null) => { * @returns A promise that resolves to the created network member. */ const addNetworkMember = async (ctx, member: MemberEntity) => { - const user = await prisma.user.findFirst({ - where: { - id: ctx.session.user.id, - }, - select: { - options: true, - }, - }); + // 1. get the user options + // 2. check if the new member is joining a organization network + const [user, memberOfOrganization] = await Promise.all([ + prisma.user.findUnique({ + where: { id: ctx.session.user.id }, + select: { options: true, network: { select: { nwid: true } } }, + }), + prisma.network.findFirst({ + where: { nwid: member.nwid }, + select: { organizationId: true, organization: { select: { settings: true } } }, + }), + ]); - const memberData = { - id: member.id, - lastSeen: new Date(), - creationTime: new Date(), - name: user.options?.addMemberIdAsName ? member.id : null, + const findNamedMember = async ({ orgId }: { orgId: string }) => { + return await prisma.network_members.findFirst({ + where: { + id: member.id, + name: { not: null }, + nwid_ref: { + organizationId: orgId, + authorId: orgId ? null : ctx.session.user.id, + }, + }, + select: { name: true }, + }); }; - // check if the new member is joining a organization network - const org = await prisma.network.findFirst({ - where: { nwid: member.nwid }, - select: { organizationId: true }, - }); + let name = null; // send webhook if the new member is joining a organization network - if (org) { + if (memberOfOrganization) { + // check if global organization member naming is enabled, and if so find the first available name + if (memberOfOrganization.organization?.settings?.renameNodeGlobally) { + const namedOrgMember = await findNamedMember({ + orgId: memberOfOrganization.organizationId, + }); + name = namedOrgMember?.name; + } try { // Send webhook await sendWebhook({ hookType: HookType.NETWORK_JOIN, - organizationId: org.organizationId, + organizationId: memberOfOrganization.organizationId, memberId: member.id, networkId: member.nwid, }); @@ -158,12 +172,19 @@ const addNetworkMember = async (ctx, member: MemberEntity) => { } } + // check if global naming is enabled, and if so find the first available name + if (user.options?.renameNodeGlobally && !memberOfOrganization.organizationId) { + const namedPrivateMember = await findNamedMember({ orgId: null }); + name = namedPrivateMember?.name; + } + return await prisma.network_members.create({ data: { - ...memberData, - nwid_ref: { - connect: { nwid: member.nwid }, - }, + id: member.id, + lastSeen: new Date(), + creationTime: new Date(), + name, + nwid_ref: { connect: { nwid: member.nwid } }, }, }); };