Fixes for avatar payloads (#5921)
This commit is contained in:
@@ -35,23 +35,7 @@ interface ICustomUsernameProps {
|
||||
setInputUsernameValue: (value: string) => void;
|
||||
onSuccessMutation?: () => void;
|
||||
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
||||
user: Pick<
|
||||
User,
|
||||
| "username"
|
||||
| "name"
|
||||
| "email"
|
||||
| "bio"
|
||||
| "avatar"
|
||||
| "timeZone"
|
||||
| "weekStart"
|
||||
| "hideBranding"
|
||||
| "theme"
|
||||
| "plan"
|
||||
| "brandColor"
|
||||
| "darkBrandColor"
|
||||
| "timeFormat"
|
||||
| "metadata"
|
||||
>;
|
||||
user: Pick<User, "username" | "metadata">;
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,23 +14,7 @@ export const UsernameAvailability = IS_SELF_HOSTED ? UsernameTextfield : Premium
|
||||
interface UsernameAvailabilityFieldProps {
|
||||
onSuccessMutation?: () => void;
|
||||
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
||||
user: Pick<
|
||||
User,
|
||||
| "username"
|
||||
| "name"
|
||||
| "email"
|
||||
| "bio"
|
||||
| "avatar"
|
||||
| "timeZone"
|
||||
| "weekStart"
|
||||
| "hideBranding"
|
||||
| "theme"
|
||||
| "plan"
|
||||
| "brandColor"
|
||||
| "darkBrandColor"
|
||||
| "timeFormat"
|
||||
| "metadata"
|
||||
>;
|
||||
user: Pick<User, "username" | "metadata">;
|
||||
}
|
||||
export const UsernameAvailabilityField = ({
|
||||
onSuccessMutation,
|
||||
|
||||
@@ -1,86 +1,72 @@
|
||||
import crypto from "crypto";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { defaultAvatarSrc } from "@lib/profile";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
// const username = req.url?.substring(1, req.url.lastIndexOf("/"));
|
||||
const username = req.query.username as string;
|
||||
const teamname = req.query.teamname as string;
|
||||
let identity;
|
||||
let linksToThisRoute = false;
|
||||
const querySchema = z
|
||||
.object({
|
||||
username: z.string(),
|
||||
teamname: z.string(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
async function getIdentityData(req: NextApiRequest) {
|
||||
const { username, teamname } = querySchema.parse(req.query);
|
||||
|
||||
if (username) {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
username: username,
|
||||
},
|
||||
select: {
|
||||
avatar: true,
|
||||
email: true,
|
||||
},
|
||||
where: { username },
|
||||
select: { avatar: true, email: true },
|
||||
});
|
||||
identity = {
|
||||
return {
|
||||
name: username,
|
||||
email: user?.email,
|
||||
avatar: user?.avatar,
|
||||
};
|
||||
linksToThisRoute =
|
||||
identity.avatar === `${CAL_URL}/${username}/avatar.png` ||
|
||||
identity.avatar === `${WEBAPP_URL}/${username}/avatar.png`;
|
||||
} else if (teamname) {
|
||||
const team = await prisma.team.findUnique({
|
||||
where: {
|
||||
slug: teamname,
|
||||
},
|
||||
select: {
|
||||
logo: true,
|
||||
},
|
||||
});
|
||||
identity = {
|
||||
name: teamname,
|
||||
shouldDefaultBeNameBased: true,
|
||||
avatar: team?.logo,
|
||||
};
|
||||
linksToThisRoute =
|
||||
identity.avatar === `${CAL_URL}/team/${teamname}/avatar.png` ||
|
||||
identity.avatar === `${WEBAPP_URL}/team/${teamname}/avatar.png`;
|
||||
}
|
||||
|
||||
const emailMd5 = crypto
|
||||
.createHash("md5")
|
||||
.update((identity?.email as string) || "guest@example.com")
|
||||
.digest("hex");
|
||||
const img = identity?.avatar;
|
||||
// If image isn't set or links to this route itself, use default avatar
|
||||
if (!img || linksToThisRoute) {
|
||||
let defaultSrc = defaultAvatarSrc({ md5: emailMd5 });
|
||||
if (identity?.shouldDefaultBeNameBased) {
|
||||
defaultSrc = getPlaceholderAvatar(null, identity.name);
|
||||
}
|
||||
res.writeHead(302, {
|
||||
Location: defaultSrc,
|
||||
if (teamname) {
|
||||
const team = await prisma.team.findUnique({
|
||||
where: { slug: teamname },
|
||||
select: { logo: true },
|
||||
});
|
||||
|
||||
res.end();
|
||||
} else if (!img.includes("data:image")) {
|
||||
res.writeHead(302, {
|
||||
Location: img,
|
||||
});
|
||||
res.end();
|
||||
} else {
|
||||
const decoded = img
|
||||
.toString()
|
||||
.replace("data:image/png;base64,", "")
|
||||
.replace("data:image/jpeg;base64,", "");
|
||||
const imageResp = Buffer.from(decoded, "base64");
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "image/png",
|
||||
"Content-Length": imageResp.length,
|
||||
});
|
||||
res.end(imageResp);
|
||||
return {
|
||||
name: teamname,
|
||||
email: null,
|
||||
avatar: team?.logo || getPlaceholderAvatar(null, teamname),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const identity = await getIdentityData(req);
|
||||
const img = identity?.avatar;
|
||||
// If image isn't set or links to this route itself, use default avatar
|
||||
if (!img) {
|
||||
res.writeHead(302, {
|
||||
Location: defaultAvatarSrc({
|
||||
md5: crypto
|
||||
.createHash("md5")
|
||||
.update(identity?.email || "guest@example.com")
|
||||
.digest("hex"),
|
||||
}),
|
||||
});
|
||||
return res.end();
|
||||
}
|
||||
|
||||
if (!img.includes("data:image")) {
|
||||
res.writeHead(302, { Location: img });
|
||||
return res.end();
|
||||
}
|
||||
|
||||
const decoded = img.toString().replace("data:image/png;base64,", "").replace("data:image/jpeg;base64,", "");
|
||||
const imageResp = Buffer.from(decoded, "base64");
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "image/png",
|
||||
"Content-Length": imageResp.length,
|
||||
});
|
||||
res.end(imageResp);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { IdentityProvider } from "@prisma/client";
|
||||
import crypto from "crypto";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { signOut } from "next-auth/react";
|
||||
import { BaseSyntheticEvent, useEffect, useRef, useState } from "react";
|
||||
import { BaseSyntheticEvent, useRef, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { ErrorCode } from "@calcom/lib/auth";
|
||||
@@ -63,14 +63,25 @@ interface DeleteAccountValues {
|
||||
totpCode: string;
|
||||
}
|
||||
|
||||
type FormValues = {
|
||||
username: string;
|
||||
avatar: string;
|
||||
name: string;
|
||||
email: string;
|
||||
bio: string;
|
||||
};
|
||||
|
||||
const ProfileView = () => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
|
||||
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
||||
const { data: avatar, isLoading: isLoadingAvatar } = trpc.viewer.avatar.useQuery();
|
||||
const mutation = trpc.viewer.updateProfile.useMutation({
|
||||
onSuccess: () => {
|
||||
showToast(t("settings_updated_successfully"), "success");
|
||||
utils.viewer.me.invalidate();
|
||||
utils.viewer.avatar.invalidate();
|
||||
setTempFormValues(null);
|
||||
},
|
||||
onError: () => {
|
||||
showToast(t("error_updating_settings"), "error");
|
||||
@@ -78,18 +89,14 @@ const ProfileView = () => {
|
||||
});
|
||||
|
||||
const [confirmPasswordOpen, setConfirmPasswordOpen] = useState(false);
|
||||
const [tempFormValues, setTempFormValues] = useState<FormValues | null>(null);
|
||||
const [confirmPasswordErrorMessage, setConfirmPasswordDeleteErrorMessage] = useState("");
|
||||
|
||||
const [deleteAccountOpen, setDeleteAccountOpen] = useState(false);
|
||||
const [hasDeleteErrors, setHasDeleteErrors] = useState(false);
|
||||
const [deleteErrorMessage, setDeleteErrorMessage] = useState("");
|
||||
|
||||
const form = useForm<DeleteAccountValues>();
|
||||
|
||||
const emailMd5 = crypto
|
||||
.createHash("md5")
|
||||
.update(user?.email || "example@example.com")
|
||||
.digest("hex");
|
||||
|
||||
const onDeleteMeSuccessMutation = async () => {
|
||||
await utils.viewer.me.invalidate();
|
||||
showToast(t("Your account was deleted"), "success");
|
||||
@@ -104,7 +111,7 @@ const ProfileView = () => {
|
||||
|
||||
const confirmPasswordMutation = trpc.viewer.auth.verifyPassword.useMutation({
|
||||
onSuccess() {
|
||||
mutation.mutate(formMethods.getValues());
|
||||
if (tempFormValues) mutation.mutate(tempFormValues);
|
||||
setConfirmPasswordOpen(false);
|
||||
},
|
||||
onError() {
|
||||
@@ -150,6 +157,7 @@ const ProfileView = () => {
|
||||
deleteMeWithoutPasswordMutation.mutate();
|
||||
}
|
||||
};
|
||||
|
||||
const onConfirm = ({ totpCode }: DeleteAccountValues, e: BaseSyntheticEvent | undefined) => {
|
||||
e?.preventDefault();
|
||||
if (isCALIdentityProviver) {
|
||||
@@ -160,38 +168,6 @@ const ProfileView = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const formMethods = useForm({
|
||||
defaultValues: {
|
||||
username: "",
|
||||
avatar: "",
|
||||
name: "",
|
||||
email: "",
|
||||
bio: "",
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
reset,
|
||||
formState: { isSubmitting, isDirty },
|
||||
} = formMethods;
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
reset(
|
||||
{
|
||||
avatar: user?.avatar || "",
|
||||
username: user?.username || "",
|
||||
name: user?.name || "",
|
||||
email: user?.email || "",
|
||||
bio: user?.bio || "",
|
||||
},
|
||||
{
|
||||
keepDirtyValues: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}, [reset, user]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const passwordRef = useRef<HTMLInputElement>(null!);
|
||||
|
||||
@@ -203,133 +179,100 @@ const ProfileView = () => {
|
||||
[ErrorCode.InternalServerError]: `${t("something_went_wrong")} ${t("please_try_again_and_contact_us")}`,
|
||||
[ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"),
|
||||
};
|
||||
const onSuccessfulUsernameUpdate = async () => {
|
||||
showToast(t("settings_updated_successfully"), "success");
|
||||
await utils.viewer.me.invalidate();
|
||||
};
|
||||
|
||||
const onErrorInUsernameUpdate = () => {
|
||||
showToast(t("error_updating_settings"), "error");
|
||||
};
|
||||
|
||||
if (isLoading || !user)
|
||||
if (isLoading || !user || isLoadingAvatar || !avatar)
|
||||
return <SkeletonLoader title={t("profile")} description={t("profile_description")} />;
|
||||
|
||||
const isDisabled = isSubmitting || !isDirty;
|
||||
const defaultValues = {
|
||||
username: user.username || "",
|
||||
avatar: avatar.avatar || "",
|
||||
name: user.name || "",
|
||||
email: user.email || "",
|
||||
bio: user.bio || "",
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title={t("profile")} description={t("profile_description", { appName: APP_NAME })} />
|
||||
<Form
|
||||
form={formMethods}
|
||||
handleSubmit={(values) => {
|
||||
if (values.email !== user?.email && isCALIdentityProviver) {
|
||||
<ProfileForm
|
||||
key={JSON.stringify(defaultValues)}
|
||||
defaultValues={defaultValues}
|
||||
onSubmit={(values) => {
|
||||
if (values.email !== user.email && isCALIdentityProviver) {
|
||||
setTempFormValues(values);
|
||||
setConfirmPasswordOpen(true);
|
||||
} else {
|
||||
mutation.mutate(values);
|
||||
}
|
||||
}}>
|
||||
<div className="flex items-center">
|
||||
<Controller
|
||||
control={formMethods.control}
|
||||
name="avatar"
|
||||
render={({ field: { value } }) => (
|
||||
<>
|
||||
<Avatar alt="" imageSrc={value} gravatarFallbackMd5={emailMd5} size="lg" />
|
||||
<div className="ml-4">
|
||||
<ImageUploader
|
||||
target="avatar"
|
||||
id="avatar-upload"
|
||||
buttonMsg={t("change_avatar")}
|
||||
handleAvatarChange={(newAvatar) => {
|
||||
formMethods.setValue("avatar", newAvatar, { shouldDirty: true });
|
||||
}}
|
||||
imageSrc={value}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}}
|
||||
extraField={
|
||||
<div className="mt-8">
|
||||
<UsernameAvailabilityField
|
||||
user={user}
|
||||
onSuccessMutation={async () => {
|
||||
showToast(t("settings_updated_successfully"), "success");
|
||||
await utils.viewer.me.invalidate();
|
||||
}}
|
||||
onErrorMutation={() => {
|
||||
showToast(t("error_updating_settings"), "error");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<hr className="my-6 border-neutral-200" />
|
||||
|
||||
<Label>{t("danger_zone")}</Label>
|
||||
{/* Delete account Dialog */}
|
||||
<Dialog open={deleteAccountOpen} onOpenChange={setDeleteAccountOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
data-testid="delete-account"
|
||||
color="destructive"
|
||||
className="mt-1 border-2"
|
||||
StartIcon={Icon.FiTrash2}>
|
||||
{t("delete_account")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
title={t("delete_account_modal_title")}
|
||||
description={t("confirm_delete_account_modal", { appName: APP_NAME })}
|
||||
type="creation"
|
||||
Icon={Icon.FiAlertTriangle}>
|
||||
<>
|
||||
<p className="mb-7">{t("delete_account_confirmation_message", { appName: APP_NAME })}</p>
|
||||
{isCALIdentityProviver && (
|
||||
<PasswordField
|
||||
data-testid="password"
|
||||
name="password"
|
||||
id="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
label="Password"
|
||||
ref={passwordRef}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<UsernameAvailabilityField
|
||||
user={user}
|
||||
onSuccessMutation={onSuccessfulUsernameUpdate}
|
||||
onErrorMutation={onErrorInUsernameUpdate}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<TextField label={t("full_name")} {...formMethods.register("name")} />
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<TextField label={t("email")} hint={t("change_email_hint")} {...formMethods.register("email")} />
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<TextField label={t("about")} hint={t("bio_hint")} {...formMethods.register("bio")} />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
disabled={isDisabled}
|
||||
color="primary"
|
||||
className="mt-8"
|
||||
type="submit"
|
||||
loading={mutation.isLoading}>
|
||||
{t("update")}
|
||||
</Button>
|
||||
{user?.twoFactorEnabled && isCALIdentityProviver && (
|
||||
<Form handleSubmit={onConfirm} className="pb-4" form={form}>
|
||||
<TwoFactor center={false} />
|
||||
</Form>
|
||||
)}
|
||||
|
||||
<hr className="my-6 border-neutral-200" />
|
||||
|
||||
<Label>{t("danger_zone")}</Label>
|
||||
{/* Delete account Dialog */}
|
||||
<Dialog open={deleteAccountOpen} onOpenChange={setDeleteAccountOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
data-testid="delete-account"
|
||||
color="destructive"
|
||||
className="mt-1 border-2"
|
||||
StartIcon={Icon.FiTrash2}>
|
||||
{t("delete_account")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
title={t("delete_account_modal_title")}
|
||||
description={t("confirm_delete_account_modal", { appName: APP_NAME })}
|
||||
type="creation"
|
||||
Icon={Icon.FiAlertTriangle}>
|
||||
<>
|
||||
<p className="mb-7">{t("delete_account_confirmation_message", { appName: APP_NAME })}</p>
|
||||
{isCALIdentityProviver && (
|
||||
<PasswordField
|
||||
data-testid="password"
|
||||
name="password"
|
||||
id="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
label="Password"
|
||||
ref={passwordRef}
|
||||
/>
|
||||
)}
|
||||
|
||||
{user?.twoFactorEnabled && isCALIdentityProviver && (
|
||||
<Form handleSubmit={onConfirm} className="pb-4" form={form}>
|
||||
<TwoFactor center={false} />
|
||||
</Form>
|
||||
)}
|
||||
|
||||
{hasDeleteErrors && <Alert severity="error" title={deleteErrorMessage} />}
|
||||
<DialogFooter>
|
||||
<Button
|
||||
color="primary"
|
||||
data-testid="delete-account-confirm"
|
||||
onClick={(e) => onConfirmButton(e)}>
|
||||
{t("delete_my_account")}
|
||||
</Button>
|
||||
<DialogClose />
|
||||
</DialogFooter>
|
||||
</>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Form>
|
||||
{hasDeleteErrors && <Alert severity="error" title={deleteErrorMessage} />}
|
||||
<DialogFooter>
|
||||
<Button
|
||||
color="primary"
|
||||
data-testid="delete-account-confirm"
|
||||
onClick={(e) => onConfirmButton(e)}>
|
||||
{t("delete_my_account")}
|
||||
</Button>
|
||||
<DialogClose />
|
||||
</DialogFooter>
|
||||
</>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* If changing email, confirm password */}
|
||||
<Dialog open={confirmPasswordOpen} onOpenChange={setConfirmPasswordOpen}>
|
||||
@@ -363,6 +306,73 @@ const ProfileView = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const ProfileForm = ({
|
||||
defaultValues,
|
||||
onSubmit,
|
||||
extraField,
|
||||
}: {
|
||||
defaultValues: FormValues;
|
||||
onSubmit: (values: FormValues) => void;
|
||||
extraField?: React.ReactNode;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const emailMd5 = crypto
|
||||
.createHash("md5")
|
||||
.update(defaultValues.email || "example@example.com")
|
||||
.digest("hex");
|
||||
|
||||
const formMethods = useForm<FormValues>({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
formState: { isSubmitting, isDirty },
|
||||
} = formMethods;
|
||||
|
||||
const isDisabled = isSubmitting || !isDirty;
|
||||
|
||||
return (
|
||||
<Form form={formMethods} handleSubmit={onSubmit}>
|
||||
<div className="flex items-center">
|
||||
<Controller
|
||||
control={formMethods.control}
|
||||
name="avatar"
|
||||
render={({ field: { value } }) => (
|
||||
<>
|
||||
<Avatar alt="" imageSrc={value} gravatarFallbackMd5={emailMd5} size="lg" />
|
||||
<div className="ml-4">
|
||||
<ImageUploader
|
||||
target="avatar"
|
||||
id="avatar-upload"
|
||||
buttonMsg={t("change_avatar")}
|
||||
handleAvatarChange={(newAvatar) => {
|
||||
formMethods.setValue("avatar", newAvatar, { shouldDirty: true });
|
||||
}}
|
||||
imageSrc={value || undefined}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{extraField}
|
||||
<div className="mt-8">
|
||||
<TextField label={t("full_name")} {...formMethods.register("name")} />
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<TextField label={t("email")} hint={t("change_email_hint")} {...formMethods.register("email")} />
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<TextField label={t("about")} hint={t("bio_hint")} {...formMethods.register("bio")} />
|
||||
</div>
|
||||
|
||||
<Button disabled={isDisabled} color="primary" className="mt-8" type="submit">
|
||||
{t("update")}
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
ProfileView.getLayout = getLayout;
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Session } from "next-auth";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getLocaleFromHeaders } from "@calcom/lib/i18n";
|
||||
import { defaultAvatarSrc } from "@calcom/lib/profile";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -87,15 +88,14 @@ async function getUserFromSession({
|
||||
if (!email) {
|
||||
return null;
|
||||
}
|
||||
const rawAvatar = user.avatar;
|
||||
// This helps to prevent reaching the 4MB payload limit by avoiding base64 and instead passing the avatar url
|
||||
// TODO: Setting avatar value to /avatar.png(which is a dynamic route) would actually reset the avatar because /avatar.png is supposed to return the value of user.avatar
|
||||
// if (user.avatar) user.avatar = `${CAL_URL}/${user.username}/avatar.png`;
|
||||
const avatar = user.avatar || defaultAvatarSrc({ email });
|
||||
user.avatar = rawAvatar ? `${WEBAPP_URL}/${user.username}/avatar.png` : defaultAvatarSrc({ email });
|
||||
|
||||
const locale = user.locale || getLocaleFromHeaders(req);
|
||||
return {
|
||||
...user,
|
||||
avatar,
|
||||
rawAvatar,
|
||||
email,
|
||||
username,
|
||||
locale,
|
||||
|
||||
@@ -6,7 +6,6 @@ import z from "zod";
|
||||
import app_RoutingForms from "@calcom/app-store/ee/routing-forms/trpc-router";
|
||||
import ethRouter from "@calcom/app-store/rainbow/trpc/router";
|
||||
import { deleteStripeCustomer } from "@calcom/app-store/stripepayment/lib/customer";
|
||||
import { getCustomerAndCheckoutSession } from "@calcom/app-store/stripepayment/lib/getCustomerAndCheckoutSession";
|
||||
import stripe, { closePayments } from "@calcom/app-store/stripepayment/lib/server";
|
||||
import { getPremiumPlanProductId } from "@calcom/app-store/stripepayment/lib/utils";
|
||||
import getApps, { getLocationGroupedOptions } from "@calcom/app-store/utils";
|
||||
@@ -19,7 +18,6 @@ import { samlTenantProduct } from "@calcom/features/ee/sso/lib/saml";
|
||||
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
||||
import getEnabledApps from "@calcom/lib/apps/getEnabledApps";
|
||||
import { ErrorCode, verifyPassword } from "@calcom/lib/auth";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
||||
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||
@@ -160,7 +158,7 @@ const loggedInViewerRouter = router({
|
||||
locale: user.locale,
|
||||
timeFormat: user.timeFormat,
|
||||
timeZone: user.timeZone,
|
||||
avatar: `${CAL_URL}/${user.username}/avatar.png`,
|
||||
avatar: user.avatar,
|
||||
createdDate: user.createdDate,
|
||||
trialEndsAt: user.trialEndsAt,
|
||||
completedOnboarding: user.completedOnboarding,
|
||||
@@ -178,6 +176,9 @@ const loggedInViewerRouter = router({
|
||||
metadata: user.metadata,
|
||||
};
|
||||
}),
|
||||
avatar: authedProcedure.query(({ ctx }) => ({
|
||||
avatar: ctx.user.rawAvatar,
|
||||
})),
|
||||
deleteMe: authedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { forwardRef, ReactElement, ReactNode, Ref, useCallback, useId, useState } from "react";
|
||||
import { Edit2, Eye, EyeOff } from "react-feather";
|
||||
import { Eye, EyeOff } from "react-feather";
|
||||
import { FieldValues, FormProvider, SubmitHandler, useFormContext, UseFormReturn } from "react-hook-form";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
import { Alert, showToast, Icon, Skeleton, Tooltip, UnstyledSelect } from "../../..";
|
||||
import { Alert, Icon, showToast, Skeleton, Tooltip, UnstyledSelect } from "../../..";
|
||||
import { HintsOrErrors } from "./HintOrErrors";
|
||||
import { Label } from "./Label";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user