Skip to content

Commit

Permalink
Merge pull request #467 from sinamics/rename-node-globally
Browse files Browse the repository at this point in the history
Implement global node naming feature for private and organization networks
  • Loading branch information
sinamics committed Aug 3, 2024
2 parents 6062994 + 4384525 commit 6b06596
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "OrganizationSettings" ADD COLUMN "renameNodeGlobally" BOOLEAN DEFAULT false;

-- AlterTable
ALTER TABLE "UserOptions" ADD COLUMN "renameNodeGlobally" BOOLEAN DEFAULT false;
18 changes: 9 additions & 9 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ model UserOptions {
// member table
deAuthorizeWarning Boolean? @default(false)
addMemberIdAsName Boolean? @default(false)
renameNodeGlobally Boolean? @default(false)
}

enum AccessLevel {
Expand Down Expand Up @@ -275,6 +276,9 @@ model Invitation {
organizations OrganizationInvitation[]
}

//
// ORGANIZATION
//
model OrganizationInvitation {
id String @id @default(cuid())
invitationId Int
Expand All @@ -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())
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}

Expand Down
39 changes: 39 additions & 0 deletions src/pages/user-settings/network/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,45 @@ const UserNetworkSetting = () => {
{t("network.memberTable.memberTableTitle")}
</p>
<div className="divider mt-0 p-0 text-gray-500"></div>
<div className="flex justify-between py-2">
<div>
<p className="font-medium">Enable global node naming</p>
<p className="text-sm text-gray-500">
When enabled, this feature will:
<ul className="list-disc list-inside mt-2">
<li>
Maintain a consistent name for each node across all networks you manage.
</li>
<li>
Update the node's name in all your networks when you rename it in one
network.
</li>
<li>
Upon member / node registration, check if the member exists in your
other networks and use the first name found.
</li>
</ul>
<p className="mt-2">
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.
</p>
</p>
</div>
<input
type="checkbox"
checked={me?.options?.renameNodeGlobally || false}
className="checkbox-primary checkbox checkbox-sm justify-self-end"
onChange={(e) => {
updateSettings(
{
renameNodeGlobally: e.target.checked,
},
{ onSuccess: () => void refetchMe() },
);
}}
/>
</div>
<div className="flex justify-between py-2">
<div>
<p className="font-medium">
Expand Down
1 change: 1 addition & 0 deletions src/server/api/routers/authRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
68 changes: 68 additions & 0 deletions src/server/api/routers/memberRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
69 changes: 45 additions & 24 deletions src/server/api/services/memberService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<MemberJoined>({
hookType: HookType.NETWORK_JOIN,
organizationId: org.organizationId,
organizationId: memberOfOrganization.organizationId,
memberId: member.id,
networkId: member.nwid,
});
Expand All @@ -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 } },
},
});
};
Expand Down

0 comments on commit 6b06596

Please sign in to comment.