From bfee74ff4e982e7fdc4bb79427469e213c812a4d Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 23 Feb 2024 17:30:49 -0800 Subject: [PATCH] Add username field to users --- ...onfig.ts => 20240223184912_ldap-config.ts} | 6 +- backend/src/db/schemas/users.ts | 2 +- .../ldap-config/ldap-config-service.ts | 1 + .../saml-config/saml-config-service.ts | 1 + backend/src/ee/services/scim/scim-service.ts | 10 +-- .../server/routes/v1/organization-router.ts | 1 + .../src/server/routes/v1/project-router.ts | 1 + .../server/routes/v2/organization-router.ts | 1 + .../routes/v2/project-membership-router.ts | 6 +- .../src/services/auth/auth-login-service.ts | 9 ++- .../src/services/auth/auth-signup-service.ts | 2 +- backend/src/services/org/org-dal.ts | 15 ++-- backend/src/services/org/org-service.ts | 8 ++- .../project-membership-dal.ts | 5 +- .../project-membership-service.ts | 7 +- .../project-membership-types.ts | 1 + frontend/src/hooks/api/users/types.ts | 3 +- .../OrgMembersSection/OrgMembersTable.tsx | 7 +- .../MemberListTab/MemberListTab.tsx | 71 +++---------------- 19 files changed, 67 insertions(+), 90 deletions(-) rename backend/src/db/migrations/{20240222182823_ldap-config.ts => 20240223184912_ldap-config.ts} (88%) diff --git a/backend/src/db/migrations/20240222182823_ldap-config.ts b/backend/src/db/migrations/20240223184912_ldap-config.ts similarity index 88% rename from backend/src/db/migrations/20240222182823_ldap-config.ts rename to backend/src/db/migrations/20240223184912_ldap-config.ts index 7add62363..ab66a04dd 100644 --- a/backend/src/db/migrations/20240222182823_ldap-config.ts +++ b/backend/src/db/migrations/20240223184912_ldap-config.ts @@ -26,9 +26,11 @@ export async function up(knex: Knex): Promise { } await knex.schema.alterTable(TableName.Users, (t) => { - t.string("username"); - t.uuid("orgId"); + t.string("username").notNullable(); + t.uuid("orgId").nullable(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); t.string("email").nullable().alter(); + t.unique(["username", "orgId"]); }); await knex(TableName.Users).update("username", knex.ref("email")); diff --git a/backend/src/db/schemas/users.ts b/backend/src/db/schemas/users.ts index 191dff6c4..ea031cda4 100644 --- a/backend/src/db/schemas/users.ts +++ b/backend/src/db/schemas/users.ts @@ -21,7 +21,7 @@ export const UsersSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), isGhost: z.boolean().default(false), - username: z.string().nullable().optional(), + username: z.string(), orgId: z.string().uuid().nullable().optional() }); diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index 21cc8e92f..eee9487a7 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -355,6 +355,7 @@ export const ldapConfigServiceFactory = ({ const newUser = await userDAL.create( { username, + orgId, firstName, lastName, authMethods: [AuthMethod.EMAIL], diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index f2ce8cf65..a293679d9 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -334,6 +334,7 @@ export const samlConfigServiceFactory = ({ user = await userDAL.transaction(async (tx) => { const newUser = await userDAL.create( { + username: email, email, firstName, lastName, diff --git a/backend/src/ee/services/scim/scim-service.ts b/backend/src/ee/services/scim/scim-service.ts index f54636654..1e54a4c46 100644 --- a/backend/src/ee/services/scim/scim-service.ts +++ b/backend/src/ee/services/scim/scim-service.ts @@ -197,7 +197,7 @@ export const scimServiceFactory = ({ return buildScimUser({ userId: membership.userId as string, - username: membership.username as string, + username: membership.username, email: membership.email ?? "", firstName: membership.firstName as string, lastName: membership.lastName as string, @@ -205,6 +205,7 @@ export const scimServiceFactory = ({ }); }; + // TODO: update SCIM endpoints to add username const createScimUser = async ({ firstName, lastName, email, orgId }: TCreateScimUserDTO) => { const org = await orgDAL.findById(orgId); @@ -250,6 +251,7 @@ export const scimServiceFactory = ({ user = await userDAL.transaction(async (tx) => { const newUser = await userDAL.create( { + username: email, email, firstName, lastName, @@ -286,7 +288,7 @@ export const scimServiceFactory = ({ return buildScimUser({ userId: user.id, - username: user.username as string, + username: user.username, firstName: user.firstName as string, lastName: user.lastName as string, email: user.email ?? "", @@ -345,7 +347,7 @@ export const scimServiceFactory = ({ return buildScimUser({ userId: membership.userId as string, - username: membership.username as string, + username: membership.username, email: membership.email ?? "", firstName: membership.firstName as string, lastName: membership.lastName as string, @@ -391,7 +393,7 @@ export const scimServiceFactory = ({ return buildScimUser({ userId: membership.userId as string, - username: membership.username as string, + username: membership.username, email: membership.email ?? "", firstName: membership.firstName as string, lastName: membership.lastName as string, diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index 6eff558fb..c39fed49c 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -58,6 +58,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { users: OrgMembershipsSchema.merge( z.object({ user: UsersSchema.pick({ + username: true, email: true, firstName: true, lastName: true, diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 0a5f8d85e..4835b665e 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -63,6 +63,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { users: ProjectMembershipsSchema.merge( z.object({ user: UsersSchema.pick({ + username: true, email: true, firstName: true, lastName: true, diff --git a/backend/src/server/routes/v2/organization-router.ts b/backend/src/server/routes/v2/organization-router.ts index 0356798c7..ba5ca3c21 100644 --- a/backend/src/server/routes/v2/organization-router.ts +++ b/backend/src/server/routes/v2/organization-router.ts @@ -24,6 +24,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { users: OrgMembershipsSchema.merge( z.object({ user: UsersSchema.pick({ + username: true, email: true, firstName: true, lastName: true, diff --git a/backend/src/server/routes/v2/project-membership-router.ts b/backend/src/server/routes/v2/project-membership-router.ts index 417fcb44c..47a7e207c 100644 --- a/backend/src/server/routes/v2/project-membership-router.ts +++ b/backend/src/server/routes/v2/project-membership-router.ts @@ -14,7 +14,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider projectId: z.string().describe("The ID of the project.") }), body: z.object({ - emails: z.string().email().array().describe("Emails of the users to add to the project.") + emails: z.string().email().array().default([]).describe("Emails of the users to add to the project."), + usernames: z.string().email().array().default([]).describe("Usernames of the users to add to the project.") }), response: { 200: z.object({ @@ -28,7 +29,8 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider projectId: req.params.projectId, actorId: req.permission.id, actor: req.permission.type, - emails: req.body.emails + emails: req.body.emails, + usernames: req.body.usernames }); await server.services.auditLog.createAuditLog({ diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index 8b4177f88..50b99677e 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -286,7 +286,14 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: }); } - user = await userDAL.create({ email, firstName, lastName, authMethods: [authMethod], isGhost: false }); + user = await userDAL.create({ + username: email, + email, + firstName, + lastName, + authMethods: [authMethod], + isGhost: false + }); } const isLinkingRequired = !user?.authMethods?.includes(authMethod); const isUserCompleted = user.isAccepted; diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts index 1e8610544..4a5384d4e 100644 --- a/backend/src/services/auth/auth-signup-service.ts +++ b/backend/src/services/auth/auth-signup-service.ts @@ -50,7 +50,7 @@ export const authSignupServiceFactory = ({ throw new Error("Failed to send verification code for complete account"); } if (!user) { - user = await userDAL.create({ authMethods: [AuthMethod.EMAIL], email, isGhost: false }); + user = await userDAL.create({ authMethods: [AuthMethod.EMAIL], username: email, email, isGhost: false }); } if (!user) throw new Error("Failed to create user"); diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index 393efd6d6..9dbcdd11d 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -57,7 +57,7 @@ export const orgDALFactory = (db: TDbClient) => { const findAllOrgMembers = async (orgId: string) => { try { const members = await db(TableName.OrgMembership) - .where({ orgId }) + .where(`${TableName.OrgMembership}.orgId`, orgId) .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) .leftJoin( TableName.UserEncryptionKey, @@ -72,22 +72,24 @@ export const orgDALFactory = (db: TDbClient) => { db.ref("roleId").withSchema(TableName.OrgMembership), db.ref("status").withSchema(TableName.OrgMembership), db.ref("email").withSchema(TableName.Users), + db.ref("username").withSchema(TableName.Users), db.ref("firstName").withSchema(TableName.Users), db.ref("lastName").withSchema(TableName.Users), db.ref("id").withSchema(TableName.Users).as("userId"), db.ref("publicKey").withSchema(TableName.UserEncryptionKey) ) .where({ isGhost: false }); // MAKE SURE USER IS NOT A GHOST USER - return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({ + + return members.map(({ email, username, firstName, lastName, userId, publicKey, ...data }) => ({ ...data, - user: { email, firstName, lastName, id: userId, publicKey } + user: { email, username, firstName, lastName, id: userId, publicKey } })); } catch (error) { throw new DatabaseError({ error, name: "Find all org members" }); } }; - const findOrgMembersByEmail = async (orgId: string, emails: string[]) => { + const findOrgMembersByUsername = async (orgId: string, usernames: string[]) => { try { const members = await db(TableName.OrgMembership) .where({ orgId }) @@ -104,6 +106,7 @@ export const orgDALFactory = (db: TDbClient) => { db.ref("role").withSchema(TableName.OrgMembership), db.ref("roleId").withSchema(TableName.OrgMembership), db.ref("status").withSchema(TableName.OrgMembership), + db.ref("username").withSchema(TableName.Users), db.ref("email").withSchema(TableName.Users), db.ref("firstName").withSchema(TableName.Users), db.ref("lastName").withSchema(TableName.Users), @@ -111,7 +114,7 @@ export const orgDALFactory = (db: TDbClient) => { db.ref("publicKey").withSchema(TableName.UserEncryptionKey) ) .where({ isGhost: false }) - .whereIn("email", emails); + .whereIn("username", usernames); return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({ ...data, user: { email, firstName, lastName, id: userId, publicKey } @@ -267,7 +270,7 @@ export const orgDALFactory = (db: TDbClient) => { findOrgById, findAllOrgsByUserId, ghostUserExists, - findOrgMembersByEmail, + findOrgMembersByUsername, findOrgGhostUser, create, updateById, diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 1e647ed3b..c12da6cf3 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -97,11 +97,11 @@ export const orgServiceFactory = ({ return members; }; - const findOrgMembersByEmail = async ({ actor, actorId, orgId, emails }: TFindOrgMembersByEmailDTO) => { + const findOrgMembersByUsername = async ({ actor, actorId, orgId, emails }: TFindOrgMembersByEmailDTO) => { const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member); - const members = await orgDAL.findOrgMembersByEmail(orgId, emails); + const members = await orgDAL.findOrgMembersByUsername(orgId, emails); return members; }; @@ -139,6 +139,7 @@ export const orgServiceFactory = ({ { isGhost: true, authMethods: [AuthMethod.EMAIL], + username: email, email, isAccepted: true }, @@ -405,6 +406,7 @@ export const orgServiceFactory = ({ // not invited before const user = await userDAL.create( { + username: inviteeEmail, email: inviteeEmail, isAccepted: false, authMethods: [AuthMethod.EMAIL], @@ -557,7 +559,7 @@ export const orgServiceFactory = ({ inviteUserToOrganization, verifyUserToOrg, updateOrg, - findOrgMembersByEmail, + findOrgMembersByUsername, createOrganization, deleteOrganizationById, deleteOrgMembership, diff --git a/backend/src/services/project-membership/project-membership-dal.ts b/backend/src/services/project-membership/project-membership-dal.ts index e0002f911..ea2902a73 100644 --- a/backend/src/services/project-membership/project-membership-dal.ts +++ b/backend/src/services/project-membership/project-membership-dal.ts @@ -25,6 +25,7 @@ export const projectMembershipDALFactory = (db: TDbClient) => { db.ref("role").withSchema(TableName.ProjectMembership), db.ref("roleId").withSchema(TableName.ProjectMembership), db.ref("isGhost").withSchema(TableName.Users), + db.ref("username").withSchema(TableName.Users), db.ref("email").withSchema(TableName.Users), db.ref("publicKey").withSchema(TableName.UserEncryptionKey), db.ref("firstName").withSchema(TableName.Users), @@ -32,9 +33,9 @@ export const projectMembershipDALFactory = (db: TDbClient) => { db.ref("id").withSchema(TableName.Users).as("userId") ) .where({ isGhost: false }); - return members.map(({ email, firstName, lastName, publicKey, isGhost, ...data }) => ({ + return members.map(({ username, email, firstName, lastName, publicKey, isGhost, ...data }) => ({ ...data, - user: { email, firstName, lastName, id: data.userId, publicKey, isGhost } + user: { username, email, firstName, lastName, id: data.userId, publicKey, isGhost } })); } catch (error) { throw new DatabaseError({ error, name: "Find all project members" }); diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index 5727d9ae8..b049d0279 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -45,7 +45,7 @@ type TProjectMembershipServiceFactoryDep = { projectMembershipDAL: TProjectMembershipDALFactory; userDAL: Pick; projectRoleDAL: Pick; - orgDAL: Pick; + orgDAL: Pick; projectDAL: Pick; projectKeyDAL: Pick; licenseService: Pick; @@ -224,6 +224,7 @@ export const projectMembershipServiceFactory = ({ actorId, actor, emails, + usernames, sendEmails = true }: TAddUsersToWorkspaceNonE2EEDTO) => { const project = await projectDAL.findById(projectId); @@ -236,7 +237,9 @@ export const projectMembershipServiceFactory = ({ const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member); - const orgMembers = await orgDAL.findOrgMembersByEmail(project.orgId, emails); + const orgMembers = await orgDAL.findOrgMembersByUsername(project.orgId, [ + ...new Set([...emails, ...usernames].map((element) => element.toLowerCase())) + ]); if (orgMembers.length !== emails.length) throw new BadRequestError({ message: "Some users are not part of org" }); diff --git a/backend/src/services/project-membership/project-membership-types.ts b/backend/src/services/project-membership/project-membership-types.ts index 634b51d77..3f42f5985 100644 --- a/backend/src/services/project-membership/project-membership-types.ts +++ b/backend/src/services/project-membership/project-membership-types.ts @@ -33,4 +33,5 @@ export type TAddUsersToWorkspaceDTO = { export type TAddUsersToWorkspaceNonE2EEDTO = { sendEmails?: boolean; emails: string[]; + usernames: string[]; } & TProjectPermission; diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index 366ee93bf..7328e13d3 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -38,7 +38,8 @@ export type UserEnc = { export type OrgUser = { id: string; user: { - email: string; + username: string; + email?: string; firstName: string; lastName: string; id: string; diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx index 3be945c2a..b0246768c 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx @@ -143,6 +143,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop ({ user: u, inviteEmail }) => u?.firstName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || u?.lastName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || + u?.username?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || u?.email?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || inviteEmail?.includes(searchMemberFilter.toLowerCase()) ), @@ -162,7 +163,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop Name - Email + Username Role @@ -173,11 +174,11 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop filterdUser?.map( ({ user: u, inviteEmail, role, roleId, id: orgMembershipId, status }) => { const name = u && u.firstName ? `${u.firstName} ${u.lastName}` : "-"; - const email = u?.email || inviteEmail; + const username = u?.username ?? inviteEmail ?? "-"; return ( {name} - {email} + {username} { const { data: wsKey } = useGetUserWsKey(workspaceId); const { data: members, isLoading: isMembersLoading } = useGetWorkspaceUsers(workspaceId); const { data: orgUsers } = useGetOrgUsers(orgId); - + const [searchMemberFilter, setSearchMemberFilter] = useState(""); const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ @@ -99,7 +94,6 @@ export const MemberListTab = () => { const { mutateAsync: addUserToWorkspace } = useAddUserToWsE2EE(); const { mutateAsync: addUserToWorkspaceNonE2EE } = useAddUserToWsNonE2EE(); - const { mutateAsync: uploadWsKey } = useUploadWsKey(); const { mutateAsync: removeUserFromWorkspace } = useDeleteUserFromWorkspace(); const { mutateAsync: updateUserWorkspaceRole } = useUpdateUserWorkspaceRole(); @@ -128,7 +122,7 @@ export const MemberListTab = () => { } else if (currentWorkspace.version === ProjectVersion.V2) { await addUserToWorkspaceNonE2EE({ projectId: workspaceId, - emails: [orgUser.user.email] + emails: [orgUser.user.username] }); } else { createNotification({ @@ -220,6 +214,7 @@ export const MemberListTab = () => { ({ user: u, inviteEmail }) => u?.firstName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || u?.lastName?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || + u?.username?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || u?.email?.toLowerCase().includes(searchMemberFilter.toLowerCase()) || inviteEmail?.includes(searchMemberFilter.toLowerCase()) ), @@ -236,40 +231,6 @@ export const MemberListTab = () => { ); }, [orgUsers, members]); - const onGrantAccess = async (grantedUserId: string, publicKey: string) => { - try { - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; - if (!PRIVATE_KEY || !wsKey) return; - - // assymmetrically decrypt symmetric key with local private key - const key = decryptAssymmetric({ - ciphertext: wsKey.encryptedKey, - nonce: wsKey.nonce, - publicKey: wsKey.sender.publicKey, - privateKey: PRIVATE_KEY - }); - - const { ciphertext, nonce } = encryptAssymmetric({ - plaintext: key, - publicKey, - privateKey: PRIVATE_KEY - }); - - await uploadWsKey({ - userId: grantedUserId, - nonce, - encryptedKey: ciphertext, - workspaceId: currentWorkspace?.id || "" - }); - } catch (err) { - console.error(err); - createNotification({ - text: "Failed to grant access to user", - type: "error" - }); - } - }; - const isLoading = isMembersLoading || isRolesLoading; return ( @@ -302,7 +263,7 @@ export const MemberListTab = () => { Name - Email + Username Role @@ -311,22 +272,20 @@ export const MemberListTab = () => { {isLoading && } {!isLoading && filterdUsers?.map( - ({ user: u, inviteEmail, id: membershipId, status, roleId, role }) => { + ({ user: u, id: membershipId, roleId, role }) => { const name = u ? `${u.firstName} ${u.lastName}` : "-"; - const email = u?.email || inviteEmail; - + const username = u?.username ?? "-"; return ( {name} - {email} + {username} {(isAllowed) => ( - <> - - {status === "completed" && user.email !== email && ( -
- -
- )} - )}