Add username field to users

This commit is contained in:
Tuan Dang
2024-02-23 17:30:49 -08:00
parent 97a7b66c6c
commit bfee74ff4e
19 changed files with 67 additions and 90 deletions

View File

@ -26,9 +26,11 @@ export async function up(knex: Knex): Promise<void> {
}
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"));

View File

@ -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()
});

View File

@ -355,6 +355,7 @@ export const ldapConfigServiceFactory = ({
const newUser = await userDAL.create(
{
username,
orgId,
firstName,
lastName,
authMethods: [AuthMethod.EMAIL],

View File

@ -334,6 +334,7 @@ export const samlConfigServiceFactory = ({
user = await userDAL.transaction(async (tx) => {
const newUser = await userDAL.create(
{
username: email,
email,
firstName,
lastName,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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({

View File

@ -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;

View File

@ -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");

View File

@ -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<TUserEncryptionKeys>(
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,

View File

@ -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,

View File

@ -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" });

View File

@ -45,7 +45,7 @@ type TProjectMembershipServiceFactoryDep = {
projectMembershipDAL: TProjectMembershipDALFactory;
userDAL: Pick<TUserDALFactory, "findById" | "findOne" | "findUserByProjectMembershipId" | "find">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "findOne">;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByEmail">;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
@ -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" });

View File

@ -33,4 +33,5 @@ export type TAddUsersToWorkspaceDTO = {
export type TAddUsersToWorkspaceNonE2EEDTO = {
sendEmails?: boolean;
emails: string[];
usernames: string[];
} & TProjectPermission;

View File

@ -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;

View File

@ -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
<THead>
<Tr>
<Th>Name</Th>
<Th>Email</Th>
<Th>Username</Th>
<Th>Role</Th>
<Th className="w-5" />
</Tr>
@ -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 (
<Tr key={`org-membership-${orgMembershipId}`} className="w-full">
<Td>{name}</Td>
<Td>{email}</Td>
<Td>{username}</Td>
<Td>
<OrgPermissionCan
I={OrgPermissionActions.Edit}

View File

@ -9,10 +9,6 @@ import { z } from "zod";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
decryptAssymmetric,
encryptAssymmetric
} from "@app/components/utilities/cryptography/crypto";
import {
Button,
DeleteActionModal,
@ -51,8 +47,7 @@ import {
useGetProjectRoles,
useGetUserWsKey,
useGetWorkspaceUsers,
useUpdateUserWorkspaceRole,
useUploadWsKey
useUpdateUserWorkspaceRole
} from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
@ -81,7 +76,7 @@ export const MemberListTab = () => {
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 = () => {
<THead>
<Tr>
<Th>Name</Th>
<Th>Email</Th>
<Th>Username</Th>
<Th>Role</Th>
<Th className="w-5" />
</Tr>
@ -311,22 +272,20 @@ export const MemberListTab = () => {
{isLoading && <TableSkeleton columns={4} innerKey="project-members" />}
{!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 (
<Tr key={`membership-${membershipId}`} className="w-full">
<Td>{name}</Td>
<Td>{email}</Td>
<Td>{username}</Td>
<Td>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Member}
>
{(isAllowed) => (
<>
<Select
<Select
value={role === "custom" ? findRoleFromId(roleId)?.slug : role}
isDisabled={userId === u?.id || !isAllowed}
className="w-40 bg-mineshaft-600"
@ -345,18 +304,6 @@ export const MemberListTab = () => {
</SelectItem>
))}
</Select>
{status === "completed" && user.email !== email && (
<div className="rounded-md border border-mineshaft-700 bg-white/5 text-white duration-200 hover:bg-primary hover:text-black">
<Button
colorSchema="secondary"
isDisabled={!isAllowed}
onClick={() => onGrantAccess(u?.id, u?.publicKey)}
>
Grant Access
</Button>
</div>
)}
</>
)}
</ProjectPermissionCan>
</Td>