feat: avatar upload (#1546)

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
Anshuman Pandey
2023-11-06 16:36:08 +05:30
committed by GitHub
parent 9d0b6cec76
commit fbd3d95034
21 changed files with 223 additions and 493 deletions

View File

@@ -311,18 +311,17 @@ export default function Navigation({
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div tabIndex={0} className="flex cursor-pointer flex-row items-center space-x-5">
<ProfileAvatar userId={session.user.id} />
{/* {session.user.image ? (
{session.user.imageUrl ? (
<Image
src={session.user.image}
width="100"
height="100"
className="ph-no-capture h-9 w-9 rounded-full"
src={session.user.imageUrl}
width="40"
height="40"
className="ph-no-capture h-10 w-10 rounded-full"
alt="Profile picture"
/>
) : (
<ProfileAvatar userId={session.user.id} />
)} */}
)}
<div>
<p className="ph-no-capture ph-no-capture -mb-0.5 text-sm font-bold text-slate-700">

View File

@@ -1,6 +1,6 @@
"use server";
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { authOptions } from "@formbricks/lib/authOptions";
import { createOrUpdateIntegration, deleteIntegration } from "@formbricks/lib/integration/service";
import { getServerSession } from "next-auth";

View File

@@ -1,6 +1,6 @@
"use server";
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { authOptions } from "@formbricks/lib/authOptions";
import { getSpreadSheets } from "@formbricks/lib/googleSheet/service";
import { getServerSession } from "next-auth";

View File

@@ -67,3 +67,17 @@ export async function disableTwoFactorAuthAction(params: TDisableTwoFactorAuthPa
return await disableTwoFactorAuth(session.user.id, params);
}
export async function updateAvatarAction(avatarUrl: string) {
const session = await getServerSession(authOptions);
if (!session) {
throw new Error("Not authenticated");
}
if (!session.user.id) {
throw new Error("User not found");
}
return await updateProfile(session.user.id, { imageUrl: avatarUrl });
}

View File

@@ -3,25 +3,91 @@
import { Button } from "@formbricks/ui/Button";
import { ProfileAvatar } from "@formbricks/ui/Avatars";
import { Session } from "next-auth";
import { useRef, useState } from "react";
import toast from "react-hot-toast";
import Image from "next/image";
import { updateAvatarAction } from "@/app/(app)/environments/[environmentId]/settings/profile/actions";
import { useRouter } from "next/navigation";
import { handleFileUpload } from "../lib";
export function EditAvatar({ session, environmentId }: { session: Session | null; environmentId: string }) {
const inputRef = useRef<HTMLInputElement>(null);
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
const handleUpload = async (file: File, environmentId: string) => {
setIsLoading(true);
try {
const { url, error } = await handleFileUpload(file, environmentId);
if (error) {
toast.error(error);
setIsLoading(false);
return;
}
await updateAvatarAction(url);
router.refresh();
} catch (err) {
toast.error("Avatar update failed. Please try again.");
setIsLoading(false);
}
setIsLoading(false);
};
export function EditAvatar({ session }: { session: Session | null }) {
return (
<div>
{/* {session?.user?.image ? (
<Image
src={AvatarPlaceholder}
width="100"
height="100"
className="h-24 w-24 rounded-full"
alt="Avatar placeholder"
/>
) : (
<ProfileAvatar userId={session!.user.id} />
)} */}
<ProfileAvatar userId={session!.user.id} />
<div className="relative h-10 w-10 overflow-hidden rounded-full">
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-30">
<svg className="h-7 w-7 animate-spin text-slate-200" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
</div>
)}
<Button className="mt-4" variant="darkCTA" disabled={true}>
{session?.user?.imageUrl ? (
<Image
src={session.user.imageUrl}
width="40"
height="40"
style={{
objectFit: "cover",
}}
className="h-10 w-10 rounded-full"
alt="Avatar placeholder"
/>
) : (
<ProfileAvatar userId={session!.user.id} />
)}
</div>
<Button
className="mt-4"
variant="darkCTA"
onClick={() => {
inputRef.current?.click();
}}>
Upload Image
<input
type="file"
id="hiddenFileInput"
ref={inputRef}
className="hidden"
accept="image/*"
onChange={async (e) => {
const file = e.target.files?.[0];
if (file) {
await handleUpload(file, environmentId);
}
}}
/>
</Button>
</div>
);

View File

@@ -0,0 +1,90 @@
export const handleFileUpload = async (
file: File,
environmentId: string
): Promise<{
error?: string;
url: string;
}> => {
if (!file) return { error: "No file provided", url: "" };
if (!file.type.startsWith("image/")) {
return { error: "Please upload an image file.", url: "" };
}
if (file.size > 10 * 1024 * 1024) {
return {
error: "File size must be less than 10 MB.",
url: "",
};
}
const payload = {
fileName: file.name,
fileType: file.type,
environmentId,
};
const response = await fetch("/api/v1/management/storage", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
// throw new Error(`Upload failed with status: ${response.status}`);
return {
error: "Upload failed. Please try again.",
url: "",
};
}
const json = await response.json();
const { data } = json;
const { signedUrl, fileUrl, signingData, presignedFields } = data;
let requestHeaders: Record<string, string> = {};
if (signingData) {
const { signature, timestamp, uuid } = signingData;
requestHeaders = {
fileType: file.type,
fileName: file.name,
environmentId: environmentId ?? "",
signature,
timestamp,
uuid,
};
}
const formData = new FormData();
if (presignedFields) {
Object.keys(presignedFields).forEach((key) => {
formData.append(key, presignedFields[key]);
});
}
// Add the actual file to be uploaded
formData.append("file", file);
const uploadResponse = await fetch(signedUrl, {
method: "POST",
...(signingData ? { headers: requestHeaders } : {}),
body: formData,
});
if (!uploadResponse.ok) {
return {
error: "Upload failed. Please try again.",
url: "",
};
}
return {
url: fileUrl,
};
};

View File

@@ -11,7 +11,8 @@ import { EditAvatar } from "./components/EditAvatar";
import AccountSecurity from "@/app/(app)/environments/[environmentId]/settings/profile/components/AccountSecurity";
import { getProfile } from "@formbricks/lib/profile/service";
export default async function ProfileSettingsPage() {
export default async function ProfileSettingsPage({ params }: { params: { environmentId: string } }) {
const { environmentId } = params;
const session = await getServerSession(authOptions);
const profile = session ? await getProfile(session.user.id) : null;
@@ -24,7 +25,7 @@ export default async function ProfileSettingsPage() {
<EditName profile={profile} />
</SettingsCard>
<SettingsCard title="Avatar" description="Assist your team in identifying you on Formbricks.">
<EditAvatar session={session} />
<EditAvatar session={session} environmentId={environmentId} />
</SettingsCard>
{profile.identityProvider === "email" && (
<SettingsCard title="Security" description="Manage your password and other security settings.">

View File

@@ -9,7 +9,7 @@ import { TProductUpdateInput } from "@formbricks/types/product";
import { TProfileUpdateInput } from "@formbricks/types/profile";
import { getServerSession } from "next-auth";
export async function updateProfileAction(updatedProfile: Partial<TProfileUpdateInput>) {
export async function updateProfileAction(updatedProfile: TProfileUpdateInput) {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");

View File

@@ -1,416 +0,0 @@
import { verifyPassword } from "@/app/lib/auth";
import { env } from "@/env.mjs";
import { prisma } from "@formbricks/database";
import { EMAIL_VERIFICATION_DISABLED, ENCRYPTION_KEY } from "@formbricks/lib/constants";
import { symmetricDecrypt, symmetricEncrypt } from "@formbricks/lib/crypto";
import { verifyToken } from "@formbricks/lib/jwt";
import { getProfileByEmail } from "@formbricks/lib/profile/service";
import type { IdentityProvider } from "@prisma/client";
import type { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import GitHubProvider from "next-auth/providers/github";
import GoogleProvider from "next-auth/providers/google";
import AzureAD from "next-auth/providers/azure-ad";
export const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
id: "credentials",
// The name to display on the sign in form (e.g. "Sign in with...")
name: "Credentials",
// The credentials is used to generate a suitable form on the sign in page.
// You can specify whatever fields you are expecting to be submitted.
// e.g. domain, username, password, 2FA token, etc.
// You can pass any HTML attribute to the <input> tag through the object.
credentials: {
email: {
label: "Email Address",
type: "email",
placeholder: "Your email address",
},
password: {
label: "Password",
type: "password",
placeholder: "Your password",
},
totpCode: { label: "Two-factor Code", type: "input", placeholder: "Code from authenticator app" },
backupCode: { label: "Backup Code", type: "input", placeholder: "Two-factor backup code" },
},
async authorize(credentials, _req) {
let user;
try {
user = await prisma.user.findUnique({
where: {
email: credentials?.email,
},
});
} catch (e) {
console.error(e);
throw Error("Internal server error. Please try again later");
}
if (!user || !credentials) {
throw new Error("No user matches the provided credentials");
}
if (!user.password) {
throw new Error("No user matches the provided credentials");
}
const isValid = await verifyPassword(credentials.password, user.password);
if (!isValid) {
throw new Error("No user matches the provided credentials");
}
if (user.twoFactorEnabled && credentials.backupCode) {
if (!ENCRYPTION_KEY) {
console.error("Missing encryption key; cannot proceed with backup code login.");
throw new Error("Internal Server Error");
}
if (!user.backupCodes) throw new Error("No backup codes found");
const backupCodes = JSON.parse(symmetricDecrypt(user.backupCodes, ENCRYPTION_KEY));
// check if user-supplied code matches one
const index = backupCodes.indexOf(credentials.backupCode.replaceAll("-", ""));
if (index === -1) throw new Error("Invalid backup code");
// delete verified backup code and re-encrypt remaining
backupCodes[index] = null;
await prisma.user.update({
where: {
id: user.id,
},
data: {
backupCodes: symmetricEncrypt(JSON.stringify(backupCodes), ENCRYPTION_KEY),
},
});
} else if (user.twoFactorEnabled) {
if (!credentials.totpCode) {
throw new Error("second factor required");
}
if (!user.twoFactorSecret) {
throw new Error("Internal Server Error");
}
if (!ENCRYPTION_KEY) {
throw new Error("Internal Server Error");
}
const secret = symmetricDecrypt(user.twoFactorSecret, ENCRYPTION_KEY);
if (secret.length !== 32) {
throw new Error("Internal Server Error");
}
const isValidToken = (await import("@formbricks/lib/totp")).totpAuthenticatorCheck(
credentials.totpCode,
secret
);
if (!isValidToken) {
throw new Error("Invalid second factor code");
}
}
return {
id: user.id,
email: user.email,
firstname: user.firstname,
lastname: user.firstname,
emailVerified: user.emailVerified,
};
},
}),
CredentialsProvider({
id: "token",
// The name to display on the sign in form (e.g. "Sign in with...")
name: "Token",
// The credentials is used to generate a suitable form on the sign in page.
// You can specify whatever fields you are expecting to be submitted.
// e.g. domain, username, password, 2FA token, etc.
// You can pass any HTML attribute to the <input> tag through the object.
credentials: {
token: {
label: "Verification Token",
type: "string",
},
},
async authorize(credentials, _req) {
let user;
try {
if (!credentials?.token) {
throw new Error("Token not found");
}
const { id } = await verifyToken(credentials?.token);
user = await prisma.user.findUnique({
where: {
id: id,
},
});
} catch (e) {
console.error(e);
throw new Error("Either a user does not match the provided token or the token is invalid");
}
if (!user) {
throw new Error("Either a user does not match the provided token or the token is invalid");
}
if (user.emailVerified) {
throw new Error("Email already verified");
}
user = await prisma.user.update({
where: {
id: user.id,
},
data: { emailVerified: new Date().toISOString() },
});
return {
id: user.id,
email: user.email,
firstname: user.firstname,
lastname: user.firstname,
emailVerified: user.emailVerified,
};
},
}),
GitHubProvider({
clientId: env.GITHUB_ID || "",
clientSecret: env.GITHUB_SECRET || "",
}),
GoogleProvider({
clientId: env.GOOGLE_CLIENT_ID || "",
clientSecret: env.GOOGLE_CLIENT_SECRET || "",
allowDangerousEmailAccountLinking: true,
}),
AzureAD({
clientId: env.AZUREAD_CLIENT_ID || "",
clientSecret: env.AZUREAD_CLIENT_SECRET || "",
tenantId: env.AZUREAD_TENANT_ID || "",
}),
],
callbacks: {
async jwt({ token }) {
const existingUser = await getProfileByEmail(token?.email!);
if (!existingUser) {
return token;
}
const additionalAttributs = {
id: existingUser.id,
createdAt: existingUser.createdAt,
onboardingCompleted: existingUser.onboardingCompleted,
name: existingUser.name,
};
return {
...token,
...additionalAttributs,
};
},
async session({ session, token }) {
// @ts-ignore
session.user.id = token?.id;
// @ts-ignore
session.user.createdAt = token?.createdAt ? new Date(token?.createdAt).toISOString() : undefined;
// @ts-ignore
session.user.onboardingCompleted = token?.onboardingCompleted;
// @ts-ignore
session.user.name = token.name || "";
return session;
},
async signIn({ user, account }: any) {
if (account.provider === "credentials" || account.provider === "token") {
if (!user.emailVerified && !EMAIL_VERIFICATION_DISABLED) {
return `/auth/verification-requested?email=${encodeURIComponent(user.email)}`;
}
return true;
}
if (!user.email || !user.name || account.type !== "oauth") {
return false;
}
if (account.provider) {
const provider = account.provider.toLowerCase().replace("-", "") as IdentityProvider;
// check if accounts for this provider / account Id already exists
const existingUserWithAccount = await prisma.user.findFirst({
include: {
accounts: {
where: {
provider: account.provider,
},
},
},
where: {
identityProvider: provider,
identityProviderAccountId: account.providerAccountId,
},
});
if (existingUserWithAccount) {
// User with this provider found
// check if email still the same
if (existingUserWithAccount.email === user.email) {
return true;
}
// user seemed to change his email within the provider
// check if user with this email already exist
// if not found just update user with new email address
// if found throw an error (TODO find better solution)
const otherUserWithEmail = await prisma.user.findFirst({
where: { email: user.email },
});
if (!otherUserWithEmail) {
await prisma.user.update({
where: { id: existingUserWithAccount.id },
data: { email: user.email },
});
return true;
}
return "/auth/login?error=Looks%20like%20you%20updated%20your%20email%20somewhere%20else.%0AA%20user%20with%20this%20new%20email%20exists%20already.";
}
// There is no existing account for this identity provider / account id
// check if user account with this email already exists
// if user already exists throw error and request password login
const existingUserWithEmail = await prisma.user.findFirst({
where: { email: user.email },
});
if (existingUserWithEmail) {
return "/auth/login?error=A%20user%20with%20this%20email%20exists%20already.";
}
await prisma.user.create({
data: {
name: user.name,
email: user.email,
emailVerified: new Date(Date.now()),
onboardingCompleted: false,
identityProvider: provider,
identityProviderAccountId: user.id as string,
accounts: {
create: [{ ...account }],
},
memberships: {
create: [
{
accepted: true,
role: "owner",
team: {
create: {
name: `${user.name}'s Team`,
products: {
create: [
{
name: "My Product",
environments: {
create: [
{
type: "production",
eventClasses: {
create: [
{
name: "New Session",
description: "Gets fired when a new session is created",
type: "automatic",
},
{
name: "Exit Intent (Desktop)",
description: "A user on Desktop leaves the website with the cursor.",
type: "automatic",
},
{
name: "50% Scroll",
description: "A user scrolled 50% of the current page",
type: "automatic",
},
],
},
attributeClasses: {
create: [
{
name: "userId",
description: "The internal ID of the person",
type: "automatic",
},
{
name: "email",
description: "The email of the person",
type: "automatic",
},
],
},
},
{
type: "development",
eventClasses: {
create: [
{
name: "New Session",
description: "Gets fired when a new session is created",
type: "automatic",
},
{
name: "Exit Intent (Desktop)",
description: "A user on Desktop leaves the website with the cursor.",
type: "automatic",
},
{
name: "50% Scroll",
description: "A user scrolled 50% of the current page",
type: "automatic",
},
],
},
attributeClasses: {
create: [
{
name: "userId",
description: "The internal ID of the person",
type: "automatic",
},
{
name: "email",
description: "The email of the person",
type: "automatic",
},
],
},
},
],
},
},
],
},
},
},
},
],
},
},
include: {
memberships: true,
},
});
return true;
}
return true;
},
},
pages: {
signIn: "/auth/login",
signOut: "/auth/logout",
error: "/auth/login", // Error code passed in query string as ?error=
},
};

View File

@@ -1,5 +1,5 @@
import NextAuth from "next-auth";
import { authOptions } from "./authOptions";
import { authOptions } from "@formbricks/lib/authOptions";
const handler = NextAuth(authOptions);

View File

@@ -1,4 +1,4 @@
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { authOptions } from "@formbricks/lib/authOptions";
import { connectAirtable, fetchAirtableAuthToken } from "@formbricks/lib/airtable/service";
import { AIR_TABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { authOptions } from "@formbricks/lib/authOptions";
import { getServerSession } from "next-auth";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import crypto from "crypto";

View File

@@ -1,4 +1,4 @@
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { authOptions } from "@formbricks/lib/authOptions";
import { getTables } from "@formbricks/lib/airtable/service";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { getIntegrationByType } from "@formbricks/lib/integration/service";

View File

@@ -1,7 +1,7 @@
import { responses } from "@/app/lib/api/response";
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { authOptions } from "@formbricks/lib/authOptions";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import getSignedUrlForPublicFile from "./lib/getSignedUrl";

View File

@@ -1,4 +1,4 @@
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { authOptions } from "@formbricks/lib/authOptions";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "imageUrl" TEXT;

View File

@@ -509,6 +509,7 @@ model User {
name String?
email String @unique
emailVerified DateTime? @map(name: "email_verified")
imageUrl String?
twoFactorSecret String?
twoFactorEnabled Boolean @default(false)
backupCodes String?

View File

@@ -1,9 +1,9 @@
import { env } from "@/env.mjs";
import { env } from "../../apps/web/env.mjs";
import { verifyPassword } from "@/app/lib/auth";
import { prisma } from "@formbricks/database";
import { EMAIL_VERIFICATION_DISABLED } from "./constants";
import { verifyToken } from "./jwt";
import { getProfileByEmail } from "./profile/service";
import { getProfileByEmail, updateProfile } from "./profile/service";
import type { IdentityProvider } from "@prisma/client";
import type { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
@@ -62,9 +62,8 @@ export const authOptions: NextAuthOptions = {
return {
id: user.id,
email: user.email,
firstname: user.firstname,
lastname: user.firstname,
emailVerified: user.emailVerified,
imageUrl: user.imageUrl,
};
},
}),
@@ -107,20 +106,9 @@ export const authOptions: NextAuthOptions = {
throw new Error("Email already verified");
}
user = await prisma.user.update({
where: {
id: user.id,
},
data: { emailVerified: new Date().toISOString() },
});
user = await updateProfile(user.id, { emailVerified: new Date() });
return {
id: user.id,
email: user.email,
firstname: user.firstname,
lastname: user.firstname,
emailVerified: user.emailVerified,
};
return user;
},
}),
GitHubProvider({
@@ -146,27 +134,16 @@ export const authOptions: NextAuthOptions = {
return token;
}
const additionalAttributs = {
id: existingUser.id,
createdAt: existingUser.createdAt,
onboardingCompleted: existingUser.onboardingCompleted,
name: existingUser.name,
};
return {
...token,
...additionalAttributs,
profile: existingUser || null,
};
},
async session({ session, token }) {
// @ts-ignore
session.user.id = token?.id;
// @ts-ignore
session.user.createdAt = token?.createdAt ? new Date(token?.createdAt).toISOString() : undefined;
// @ts-ignore
session.user.onboardingCompleted = token?.onboardingCompleted;
// @ts-ignore
session.user.name = token.name || "";
session.user = token.profile;
return session;
},
@@ -210,15 +187,10 @@ export const authOptions: NextAuthOptions = {
// check if user with this email already exist
// if not found just update user with new email address
// if found throw an error (TODO find better solution)
const otherUserWithEmail = await prisma.user.findFirst({
where: { email: user.email },
});
const otherUserWithEmail = await getProfileByEmail(user.email);
if (!otherUserWithEmail) {
await prisma.user.update({
where: { id: existingUserWithAccount.id },
data: { email: user.email },
});
await updateProfile(existingUserWithAccount.id, { email: user.email });
return true;
}
return "/auth/login?error=Looks%20like%20you%20updated%20your%20email%20somewhere%20else.%0AA%20user%20with%20this%20new%20email%20exists%20already.";
@@ -227,9 +199,7 @@ export const authOptions: NextAuthOptions = {
// There is no existing account for this identity provider / account id
// check if user account with this email already exists
// if user already exists throw error and request password login
const existingUserWithEmail = await prisma.user.findFirst({
where: { email: user.email },
});
const existingUserWithEmail = await getProfileByEmail(user.email);
if (existingUserWithEmail) {
return "/auth/login?error=A%20user%20with%20this%20email%20exists%20already.";

View File

@@ -23,6 +23,8 @@ const responseSelection = {
id: true,
name: true,
email: true,
emailVerified: true,
imageUrl: true,
createdAt: true,
updatedAt: true,
onboardingCompleted: true,
@@ -102,10 +104,7 @@ const getAdminMemberships = (memberships: TMembership[]): TMembership[] =>
memberships.filter((membership) => membership.role === "admin");
// function to update a user's profile
export const updateProfile = async (
personId: string,
data: Partial<TProfileUpdateInput>
): Promise<TProfile> => {
export const updateProfile = async (personId: string, data: TProfileUpdateInput): Promise<TProfile> => {
validateInputs([personId, ZId], [data, ZProfileUpdateInput.partial()]);
try {

View File

@@ -17,6 +17,8 @@ export const ZProfile = z.object({
id: z.string(),
name: z.string().nullable(),
email: z.string(),
emailVerified: z.date().nullable(),
imageUrl: z.string().url().nullable(),
twoFactorEnabled: z.boolean(),
identityProvider: z.enum(["email", "google", "github", "azuread"]),
createdAt: z.date(),
@@ -30,9 +32,11 @@ export type TProfile = z.infer<typeof ZProfile>;
export const ZProfileUpdateInput = z.object({
name: z.string().nullish(),
email: z.string().optional(),
emailVerified: z.date().nullish(),
onboardingCompleted: z.boolean().optional(),
role: ZRole.optional(),
objective: ZProfileObjective.nullish(),
imageUrl: z.string().url().nullish(),
});
export type TProfileUpdateInput = z.infer<typeof ZProfileUpdateInput>;
@@ -40,6 +44,7 @@ export type TProfileUpdateInput = z.infer<typeof ZProfileUpdateInput>;
export const ZProfileCreateInput = z.object({
name: z.string().optional(),
email: z.string(),
emailVerified: z.date().optional(),
onboardingCompleted: z.boolean().optional(),
role: ZRole.optional(),
objective: ZProfileObjective.nullish(),

View File

@@ -89,7 +89,6 @@ const uploadFile = async (
url: fileUrl,
};
} catch (error) {
console.log({ error });
throw error;
}
};