Fix/login with provider (#2594)
This commit is contained in:
		
							parent
							
								
									e260ba0e49
								
							
						
					
					
						commit
						4b75bf7cce
					
				
					 5 changed files with 189 additions and 5 deletions
				
			
		
							
								
								
									
										85
									
								
								apps/web/lib/auth/next-auth-custom-adapter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								apps/web/lib/auth/next-auth-custom-adapter.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,85 @@ | |||
| import { Account, IdentityProvider, Prisma, PrismaClient, User, VerificationToken } from "@prisma/client"; | ||||
| 
 | ||||
| import { identityProviderNameMap } from "@lib/auth"; | ||||
| 
 | ||||
| /** @return { import("next-auth/adapters").Adapter } */ | ||||
| export default function CalComAdapter(prismaClient: PrismaClient) { | ||||
|   return { | ||||
|     createUser: (data: Prisma.UserCreateInput) => prismaClient.user.create({ data }), | ||||
|     getUser: (id: User["id"]) => prismaClient.user.findUnique({ where: { id } }), | ||||
|     getUserByEmail: (email: User["email"]) => prismaClient.user.findUnique({ where: { email } }), | ||||
|     async getUserByAccount(provider_providerAccountId: { | ||||
|       providerAccountId: Account["providerAccountId"]; | ||||
|       provider: User["identityProvider"]; | ||||
|     }) { | ||||
|       let _account; | ||||
|       const account = await prismaClient.account.findUnique({ | ||||
|         where: { | ||||
|           provider_providerAccountId, | ||||
|         }, | ||||
|         select: { user: true }, | ||||
|       }); | ||||
|       if (account) { | ||||
|         return (_account = account === null || account === void 0 ? void 0 : account.user) !== null && | ||||
|           _account !== void 0 | ||||
|           ? _account | ||||
|           : null; | ||||
|       } | ||||
| 
 | ||||
|       // NOTE: this code it's our fallback to users without Account but credentials in User Table
 | ||||
|       // We should remove this code after all googles tokens have expired
 | ||||
|       const provider = provider_providerAccountId?.provider.toUpperCase() as IdentityProvider; | ||||
|       if (["GOOGLE", "SAML"].indexOf(provider) < 0) { | ||||
|         return null; | ||||
|       } | ||||
|       const obtainProvider = identityProviderNameMap[provider].toUpperCase() as IdentityProvider; | ||||
|       const user = await prismaClient.user.findFirst({ | ||||
|         where: { | ||||
|           identityProviderId: provider_providerAccountId?.providerAccountId, | ||||
|           identityProvider: obtainProvider, | ||||
|         }, | ||||
|       }); | ||||
|       return user || null; | ||||
|     }, | ||||
|     updateUser: ({ id, ...data }: Prisma.UserUncheckedCreateInput) => | ||||
|       prismaClient.user.update({ where: { id }, data }), | ||||
|     deleteUser: (id: User["id"]) => prismaClient.user.delete({ where: { id } }), | ||||
|     async createVerificationToken(data: VerificationToken) { | ||||
|       const { id: _, ...verificationToken } = await prismaClient.verificationToken.create({ | ||||
|         data, | ||||
|       }); | ||||
|       return verificationToken; | ||||
|     }, | ||||
|     async useVerificationToken(identifier_token: Prisma.VerificationTokenIdentifierTokenCompoundUniqueInput) { | ||||
|       try { | ||||
|         const { id: _, ...verificationToken } = await prismaClient.verificationToken.delete({ | ||||
|           where: { identifier_token }, | ||||
|         }); | ||||
|         return verificationToken; | ||||
|       } catch (error) { | ||||
|         // If token already used/deleted, just return null
 | ||||
|         // https://www.prisma.io/docs/reference/api-reference/error-reference#p2025
 | ||||
|         // @ts-ignore
 | ||||
|         if (error.code === "P2025") return null; | ||||
|         throw error; | ||||
|       } | ||||
|     }, | ||||
|     linkAccount: (data: Prisma.AccountCreateInput) => prismaClient.account.create({ data }), | ||||
|     // @NOTE: All methods below here are not being used but leaved if they are required
 | ||||
|     unlinkAccount: (provider_providerAccountId: Prisma.AccountProviderProviderAccountIdCompoundUniqueInput) => | ||||
|       prismaClient.account.delete({ where: { provider_providerAccountId } }), | ||||
|     async getSessionAndUser(sessionToken: string) { | ||||
|       const userAndSession = await prismaClient.session.findUnique({ | ||||
|         where: { sessionToken }, | ||||
|         include: { user: true }, | ||||
|       }); | ||||
|       if (!userAndSession) return null; | ||||
|       const { user, ...session } = userAndSession; | ||||
|       return { user, session }; | ||||
|     }, | ||||
|     createSession: (data: Prisma.SessionCreateInput) => prismaClient.session.create({ data }), | ||||
|     updateSession: (data: Prisma.SessionWhereUniqueInput) => | ||||
|       prismaClient.session.update({ where: { sessionToken: data.sessionToken }, data }), | ||||
|     deleteSession: (sessionToken: string) => prismaClient.session.delete({ where: { sessionToken } }), | ||||
|   }; | ||||
| } | ||||
|  | @ -18,6 +18,7 @@ import { serverConfig } from "@calcom/lib/serverConfig"; | |||
| import ImpersonationProvider from "@ee/lib/impersonation/ImpersonationProvider"; | ||||
| 
 | ||||
| import { ErrorCode, verifyPassword } from "@lib/auth"; | ||||
| import CalComAdapter from "@lib/auth/next-auth-custom-adapter"; | ||||
| import prisma from "@lib/prisma"; | ||||
| import { randomString } from "@lib/random"; | ||||
| import { hostedCal, isSAMLLoginEnabled, samlLoginUrl } from "@lib/saml"; | ||||
|  | @ -185,9 +186,10 @@ if (true) { | |||
|     }) | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| const calcomAdapter = CalComAdapter(prisma); | ||||
| export default NextAuth({ | ||||
|   adapter: PrismaAdapter(prisma), | ||||
|   // @ts-ignore
 | ||||
|   adapter: calcomAdapter, | ||||
|   session: { | ||||
|     strategy: "jwt", | ||||
|   }, | ||||
|  | @ -246,7 +248,6 @@ export default NextAuth({ | |||
|         if (account.provider === "saml") { | ||||
|           idP = IdentityProvider.SAML; | ||||
|         } | ||||
| 
 | ||||
|         const existingUser = await prisma.user.findFirst({ | ||||
|           where: { | ||||
|             AND: [ | ||||
|  | @ -292,6 +293,7 @@ export default NextAuth({ | |||
|     }, | ||||
|     async signIn(params) { | ||||
|       const { user, account, profile } = params; | ||||
| 
 | ||||
|       if (account.provider === "email") { | ||||
|         return true; | ||||
|       } | ||||
|  | @ -323,10 +325,20 @@ export default NextAuth({ | |||
|         if (!user.email_verified) { | ||||
|           return "/auth/error?error=unverified-email"; | ||||
|         } | ||||
|         // Only google oauth on this path
 | ||||
|         const provider = account.provider.toUpperCase() as IdentityProvider; | ||||
| 
 | ||||
|         const existingUser = await prisma.user.findFirst({ | ||||
|           include: { | ||||
|             accounts: { | ||||
|               where: { | ||||
|             AND: [{ identityProvider: idP }, { identityProviderId: user.id as string }], | ||||
|                 provider: account.provider, | ||||
|               }, | ||||
|             }, | ||||
|           }, | ||||
|           where: { | ||||
|             identityProvider: provider, | ||||
|             identityProviderId: account.providerAccountId, | ||||
|           }, | ||||
|         }); | ||||
| 
 | ||||
|  | @ -334,6 +346,17 @@ export default NextAuth({ | |||
|           // In this case there's an existing user and their email address
 | ||||
|           // hasn't changed since they last logged in.
 | ||||
|           if (existingUser.email === user.email) { | ||||
|             try { | ||||
|               // If old user without Account entry we link their google account
 | ||||
|               if (existingUser.accounts.length === 0) { | ||||
|                 const linkAccountWithUserData = { ...account, userId: existingUser.id }; | ||||
|                 await calcomAdapter.linkAccount(linkAccountWithUserData); | ||||
|               } | ||||
|             } catch (error) { | ||||
|               if (error instanceof Error) { | ||||
|                 console.error("Error while linking account of already existing user"); | ||||
|               } | ||||
|             } | ||||
|             return true; | ||||
|           } | ||||
| 
 | ||||
|  | @ -394,7 +417,7 @@ export default NextAuth({ | |||
|           return "/auth/error?error=use-identity-login"; | ||||
|         } | ||||
| 
 | ||||
|         await prisma.user.create({ | ||||
|         const newUser = await prisma.user.create({ | ||||
|           data: { | ||||
|             // Slugify the incoming name and append a few random characters to
 | ||||
|             // prevent conflicts for users with the same name.
 | ||||
|  | @ -406,6 +429,8 @@ export default NextAuth({ | |||
|             identityProviderId: user.id as string, | ||||
|           }, | ||||
|         }); | ||||
|         const linkAccountNewUserData = { ...account, userId: newUser.id }; | ||||
|         await calcomAdapter.linkAccount(linkAccountNewUserData); | ||||
| 
 | ||||
|         return true; | ||||
|       } | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import { CheckIcon } from "@heroicons/react/outline"; | ||||
| import { GetServerSidePropsContext } from "next"; | ||||
| import { useSession, signOut } from "next-auth/react"; | ||||
| import { getCookieParser } from "next/dist/server/api-utils"; | ||||
| import Link from "next/link"; | ||||
| import { useRouter } from "next/router"; | ||||
| import { useEffect } from "react"; | ||||
|  | @ -51,6 +52,11 @@ export default function Logout(props: Props) { | |||
| 
 | ||||
| export async function getServerSideProps(context: GetServerSidePropsContext) { | ||||
|   const ssr = await ssrInit(context); | ||||
|   // Deleting old cookie manually, remove this code after all existing cookies have expired
 | ||||
|   context.res.setHeader( | ||||
|     "Set-Cookie", | ||||
|     "next-auth.session-token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;" | ||||
|   ); | ||||
| 
 | ||||
|   return { | ||||
|     props: { | ||||
|  |  | |||
|  | @ -0,0 +1,39 @@ | |||
| -- CreateTable | ||||
| CREATE TABLE "Account" ( | ||||
|     "id" TEXT NOT NULL, | ||||
|     "userId" INTEGER NOT NULL, | ||||
|     "type" TEXT NOT NULL, | ||||
|     "provider" TEXT NOT NULL, | ||||
|     "providerAccountId" TEXT NOT NULL, | ||||
|     "refresh_token" TEXT, | ||||
|     "access_token" TEXT, | ||||
|     "expires_at" INTEGER, | ||||
|     "token_type" TEXT, | ||||
|     "scope" TEXT, | ||||
|     "id_token" TEXT, | ||||
|     "session_state" TEXT, | ||||
| 
 | ||||
|     CONSTRAINT "Account_pkey" PRIMARY KEY ("id") | ||||
| ); | ||||
| 
 | ||||
| -- CreateTable | ||||
| CREATE TABLE "Session" ( | ||||
|     "id" TEXT NOT NULL, | ||||
|     "sessionToken" TEXT NOT NULL, | ||||
|     "userId" INTEGER NOT NULL, | ||||
|     "expires" TIMESTAMP(3) NOT NULL, | ||||
| 
 | ||||
|     CONSTRAINT "Session_pkey" PRIMARY KEY ("id") | ||||
| ); | ||||
| 
 | ||||
| -- CreateIndex | ||||
| CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); | ||||
| 
 | ||||
| -- CreateIndex | ||||
| CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); | ||||
| 
 | ||||
| -- AddForeignKey | ||||
| ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; | ||||
| 
 | ||||
| -- AddForeignKey | ||||
| ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; | ||||
|  | @ -164,6 +164,8 @@ model User { | |||
|   impersonatedUsers   Impersonations[]     @relation("impersonated_user") | ||||
|   impersonatedBy      Impersonations[]     @relation("impersonated_by_user") | ||||
|   apiKeys             ApiKey[] | ||||
|   accounts            Account[] | ||||
|   sessions            Session[] | ||||
| 
 | ||||
|   @@map(name: "users") | ||||
| } | ||||
|  | @ -403,3 +405,30 @@ model ApiKey { | |||
|   hashedKey  String    @unique() | ||||
|   user       User?     @relation(fields: [userId], references: [id], onDelete: Cascade) | ||||
| } | ||||
| 
 | ||||
| model Account { | ||||
|   id                String  @id @default(cuid()) | ||||
|   userId            Int | ||||
|   type              String | ||||
|   provider          String | ||||
|   providerAccountId String | ||||
|   refresh_token     String? @db.Text | ||||
|   access_token      String? @db.Text | ||||
|   expires_at        Int? | ||||
|   token_type        String? | ||||
|   scope             String? | ||||
|   id_token          String? @db.Text | ||||
|   session_state     String? | ||||
| 
 | ||||
|   user User? @relation(fields: [userId], references: [id], onDelete: Cascade) | ||||
| 
 | ||||
|   @@unique([provider, providerAccountId]) | ||||
| } | ||||
| 
 | ||||
| model Session { | ||||
|   id           String   @id @default(cuid()) | ||||
|   sessionToken String   @unique | ||||
|   userId       Int | ||||
|   expires      DateTime | ||||
|   user         User?    @relation(fields: [userId], references: [id], onDelete: Cascade) | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue
	
	 alannnc
						alannnc