diff --git a/apps/web/app/(app)/environments/[environmentId]/components/Navigation.tsx b/apps/web/app/(app)/environments/[environmentId]/components/Navigation.tsx index 03e3078369..e0a29843bd 100644 --- a/apps/web/app/(app)/environments/[environmentId]/components/Navigation.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/components/Navigation.tsx @@ -311,18 +311,17 @@ export default function Navigation({
- - {/* {session.user.image ? ( + {session.user.imageUrl ? ( Profile picture ) : ( - )} */} + )}

diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/actions.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/actions.ts index 57ee1b1f5f..7d62b261f0 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/actions.ts @@ -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"; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/actions.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/actions.ts index 97a9a9858b..5f2f07cae1 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/actions.ts @@ -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"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/profile/actions.ts b/apps/web/app/(app)/environments/[environmentId]/settings/profile/actions.ts index 5bbbcccc9f..8a7473ec03 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/profile/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/settings/profile/actions.ts @@ -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 }); +} diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/profile/components/EditAvatar.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/profile/components/EditAvatar.tsx index 3fdb90784d..79c7aa00c1 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/profile/components/EditAvatar.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/profile/components/EditAvatar.tsx @@ -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(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 (

- {/* {session?.user?.image ? ( - Avatar placeholder - ) : ( - - )} */} - +
+ {isLoading && ( +
+ + + + +
+ )} -
+ +
); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/profile/lib.ts b/apps/web/app/(app)/environments/[environmentId]/settings/profile/lib.ts new file mode 100644 index 0000000000..d36d261ab1 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/settings/profile/lib.ts @@ -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 = {}; + + 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, + }; +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/profile/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/profile/page.tsx index d206331811..5a47ce585e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/profile/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/profile/page.tsx @@ -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() { - + {profile.identityProvider === "email" && ( diff --git a/apps/web/app/(app)/onboarding/actions.ts b/apps/web/app/(app)/onboarding/actions.ts index 731a416aa9..449bb5c7ad 100644 --- a/apps/web/app/(app)/onboarding/actions.ts +++ b/apps/web/app/(app)/onboarding/actions.ts @@ -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) { +export async function updateProfileAction(updatedProfile: TProfileUpdateInput) { const session = await getServerSession(authOptions); if (!session) throw new AuthorizationError("Not authorized"); diff --git a/apps/web/app/api/auth/[...nextauth]/authOptions.ts b/apps/web/app/api/auth/[...nextauth]/authOptions.ts deleted file mode 100644 index 2a593fbec3..0000000000 --- a/apps/web/app/api/auth/[...nextauth]/authOptions.ts +++ /dev/null @@ -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 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 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= - }, -}; diff --git a/apps/web/app/api/auth/[...nextauth]/route.ts b/apps/web/app/api/auth/[...nextauth]/route.ts index f1cc59ec9c..2e92566b35 100644 --- a/apps/web/app/api/auth/[...nextauth]/route.ts +++ b/apps/web/app/api/auth/[...nextauth]/route.ts @@ -1,5 +1,5 @@ import NextAuth from "next-auth"; -import { authOptions } from "./authOptions"; +import { authOptions } from "@formbricks/lib/authOptions"; const handler = NextAuth(authOptions); diff --git a/apps/web/app/api/v1/integrations/airtable/callback/route.ts b/apps/web/app/api/v1/integrations/airtable/callback/route.ts index ab497df249..da6f287a4d 100644 --- a/apps/web/app/api/v1/integrations/airtable/callback/route.ts +++ b/apps/web/app/api/v1/integrations/airtable/callback/route.ts @@ -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"; diff --git a/apps/web/app/api/v1/integrations/airtable/route.ts b/apps/web/app/api/v1/integrations/airtable/route.ts index f385004c80..6734e42d20 100644 --- a/apps/web/app/api/v1/integrations/airtable/route.ts +++ b/apps/web/app/api/v1/integrations/airtable/route.ts @@ -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"; diff --git a/apps/web/app/api/v1/integrations/airtable/tables/route.ts b/apps/web/app/api/v1/integrations/airtable/tables/route.ts index fdb112f7fe..23f5520d8a 100644 --- a/apps/web/app/api/v1/integrations/airtable/tables/route.ts +++ b/apps/web/app/api/v1/integrations/airtable/tables/route.ts @@ -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"; diff --git a/apps/web/app/api/v1/management/storage/route.ts b/apps/web/app/api/v1/management/storage/route.ts index 96eccc403c..9afd09e181 100644 --- a/apps/web/app/api/v1/management/storage/route.ts +++ b/apps/web/app/api/v1/management/storage/route.ts @@ -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"; diff --git a/apps/web/app/storage/[environmentId]/[accessType]/[fileName]/route.ts b/apps/web/app/storage/[environmentId]/[accessType]/[fileName]/route.ts index ebc636d216..a99d791838 100644 --- a/apps/web/app/storage/[environmentId]/[accessType]/[fileName]/route.ts +++ b/apps/web/app/storage/[environmentId]/[accessType]/[fileName]/route.ts @@ -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"; diff --git a/packages/database/migrations/20231102045537_adds_image_url_column_in_the_user_table/migration.sql b/packages/database/migrations/20231102045537_adds_image_url_column_in_the_user_table/migration.sql new file mode 100644 index 0000000000..67117c041c --- /dev/null +++ b/packages/database/migrations/20231102045537_adds_image_url_column_in_the_user_table/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "imageUrl" TEXT; diff --git a/packages/database/schema.prisma b/packages/database/schema.prisma index 1025e4cf69..0d0ddbfe11 100644 --- a/packages/database/schema.prisma +++ b/packages/database/schema.prisma @@ -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? diff --git a/packages/lib/authOptions.ts b/packages/lib/authOptions.ts index 71c9c87cb3..bf61aa2fd5 100644 --- a/packages/lib/authOptions.ts +++ b/packages/lib/authOptions.ts @@ -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."; diff --git a/packages/lib/profile/service.ts b/packages/lib/profile/service.ts index ba19553699..cc7824d095 100644 --- a/packages/lib/profile/service.ts +++ b/packages/lib/profile/service.ts @@ -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 -): Promise => { +export const updateProfile = async (personId: string, data: TProfileUpdateInput): Promise => { validateInputs([personId, ZId], [data, ZProfileUpdateInput.partial()]); try { diff --git a/packages/types/profile.ts b/packages/types/profile.ts index 406be81da8..36c3093a09 100644 --- a/packages/types/profile.ts +++ b/packages/types/profile.ts @@ -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; 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; @@ -40,6 +44,7 @@ export type TProfileUpdateInput = z.infer; 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(), diff --git a/packages/ui/FileInput/lib/fileUpload.ts b/packages/ui/FileInput/lib/fileUpload.ts index ef9cb6d98a..4d4fcc755c 100644 --- a/packages/ui/FileInput/lib/fileUpload.ts +++ b/packages/ui/FileInput/lib/fileUpload.ts @@ -89,7 +89,6 @@ const uploadFile = async ( url: fileUrl, }; } catch (error) { - console.log({ error }); throw error; } };