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(),