diff --git a/packages/features/users/components/UserTable/EditSheet/EditUserForm.tsx b/packages/features/users/components/UserTable/EditSheet/EditUserForm.tsx index f5b9375635..686b67877f 100644 --- a/packages/features/users/components/UserTable/EditSheet/EditUserForm.tsx +++ b/packages/features/users/components/UserTable/EditSheet/EditUserForm.tsx @@ -30,6 +30,7 @@ type MembershipOption = { const editSchema = z.object({ name: z.string(), + username: z.string(), email: z.string().email(), avatar: z.string(), bio: z.string(), @@ -59,6 +60,7 @@ export function EditForm({ resolver: zodResolver(editSchema), defaultValues: { name: selectedUser?.name ?? "", + username: selectedUser?.username ?? "", email: selectedUser?.email ?? "", avatar: avatarUrl, bio: selectedUser?.bio ?? "", @@ -121,6 +123,7 @@ export function EditForm({ mutation.mutate({ userId: selectedUser?.id ?? "", role: values.role, + username: values.username, name: values.name, email: values.email, avatar: values.avatar, @@ -157,6 +160,7 @@ export function EditForm({
+ diff --git a/packages/lib/server/checkRegularUsername.ts b/packages/lib/server/checkRegularUsername.ts index 89e61810ea..e039091268 100644 --- a/packages/lib/server/checkRegularUsername.ts +++ b/packages/lib/server/checkRegularUsername.ts @@ -4,6 +4,7 @@ import { ProfileRepository } from "./repository/profile"; export async function checkRegularUsername(_username: string, currentOrgDomain?: string | null) { const username = slugify(_username); + const premium = !!process.env.NEXT_PUBLIC_IS_E2E && username.length < 5; const profiles = currentOrgDomain @@ -13,7 +14,7 @@ export async function checkRegularUsername(_username: string, currentOrgDomain?: }) : null; - const user = profiles ? profiles[0].user : null; + const user = profiles?.length ? profiles[0].user : null; if (user) { return { diff --git a/packages/trpc/server/routers/viewer/organizations/updateUser.handler.ts b/packages/trpc/server/routers/viewer/organizations/updateUser.handler.ts index 7c39576e1a..17cc5d2fb9 100644 --- a/packages/trpc/server/routers/viewer/organizations/updateUser.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/updateUser.handler.ts @@ -1,7 +1,11 @@ +import type { Prisma, PrismaPromise, User, Membership, Profile } from "@prisma/client"; + +import { checkRegularUsername } from "@calcom/lib/server/checkRegularUsername"; import { isOrganisationAdmin, isOrganisationOwner } from "@calcom/lib/server/queries/organisations"; import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image"; import { prisma } from "@calcom/prisma"; import { MembershipRole } from "@calcom/prisma/enums"; +import { uploadAvatar } from "@calcom/trpc/server/routers/loggedInViewer/updateProfile.handler"; import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; import { TRPCError } from "@trpc/server"; @@ -32,6 +36,7 @@ const applyRoleToAllTeams = async (userId: number, teamIds: number[], role: Memb export const updateUserHandler = async ({ ctx, input }: UpdateUserOptions) => { const { user } = ctx; const { id: userId, organizationId } = user; + if (!organizationId) throw new TRPCError({ code: "UNAUTHORIZED", message: "You must be a memeber of an organizaiton" }); @@ -74,30 +79,58 @@ export const updateUserHandler = async ({ ctx, input }: UpdateUserOptions) => { }, }, }, + user: { + select: { + username: true, + }, + }, }, }); if (!requestedMember) throw new TRPCError({ code: "UNAUTHORIZED", message: "User does not belong to your organization" }); - let avatar = input.avatar; - if (input.avatar) { - avatar = await resizeBase64Image(input.avatar); + const hasUsernameUpdated = input.username !== requestedMember.user.username; + + if (input.username && hasUsernameUpdated && user.profile.organization?.slug) { + const checkRegularUsernameRes = await checkRegularUsername( + input.username, + user.profile.organization.slug + ); + if (!checkRegularUsernameRes.available) { + throw new TRPCError({ code: "BAD_REQUEST", message: checkRegularUsernameRes.message }); + } + } + + const data: Prisma.UserUpdateInput = { + bio: input.bio, + email: input.email, + name: input.name, + timeZone: input.timeZone, + username: input.username, + avatar: undefined, + }; + + if (input.avatar && input.avatar.startsWith("data:image/png;base64,")) { + const avatar = await resizeBase64Image(input.avatar); + data.avatarUrl = await uploadAvatar({ + avatar, + userId: user.id, + }); + data.avatar = avatar; + } + if (input.avatar === "") { + data.avatarUrl = null; + data.avatar = null; } // Update user - await prisma.$transaction([ + const transactions: PrismaPromise[] = [ prisma.user.update({ where: { id: input.userId, }, - data: { - bio: input.bio, - email: input.email, - name: input.name, - timeZone: input.timeZone, - avatar, - }, + data, }), prisma.membership.update({ where: { @@ -110,7 +143,25 @@ export const updateUserHandler = async ({ ctx, input }: UpdateUserOptions) => { role: input.role, }, }), - ]); + ]; + + if (hasUsernameUpdated) { + transactions.push( + prisma.profile.update({ + where: { + userId_organizationId: { + userId: input.userId, + organizationId, + }, + }, + data: { + username: input.username, + }, + }) + ); + } + + await prisma.$transaction(transactions); if (input.role === MembershipRole.ADMIN || input.role === MembershipRole.OWNER) { const teamIds = requestedMember.team.children diff --git a/packages/trpc/server/routers/viewer/organizations/updateUser.schema.ts b/packages/trpc/server/routers/viewer/organizations/updateUser.schema.ts index e12658864e..1f0fde2b32 100644 --- a/packages/trpc/server/routers/viewer/organizations/updateUser.schema.ts +++ b/packages/trpc/server/routers/viewer/organizations/updateUser.schema.ts @@ -2,6 +2,7 @@ import { z } from "zod"; export const ZUpdateUserInputSchema = z.object({ userId: z.number(), + username: z.string().optional(), bio: z.string().optional(), name: z.string().optional(), email: z.string().optional(),