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 ImpersonationProvider from "@ee/lib/impersonation/ImpersonationProvider";
|
||||||
|
|
||||||
import { ErrorCode, verifyPassword } from "@lib/auth";
|
import { ErrorCode, verifyPassword } from "@lib/auth";
|
||||||
|
import CalComAdapter from "@lib/auth/next-auth-custom-adapter";
|
||||||
import prisma from "@lib/prisma";
|
import prisma from "@lib/prisma";
|
||||||
import { randomString } from "@lib/random";
|
import { randomString } from "@lib/random";
|
||||||
import { hostedCal, isSAMLLoginEnabled, samlLoginUrl } from "@lib/saml";
|
import { hostedCal, isSAMLLoginEnabled, samlLoginUrl } from "@lib/saml";
|
||||||
|
@ -185,9 +186,10 @@ if (true) {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const calcomAdapter = CalComAdapter(prisma);
|
||||||
export default NextAuth({
|
export default NextAuth({
|
||||||
adapter: PrismaAdapter(prisma),
|
// @ts-ignore
|
||||||
|
adapter: calcomAdapter,
|
||||||
session: {
|
session: {
|
||||||
strategy: "jwt",
|
strategy: "jwt",
|
||||||
},
|
},
|
||||||
|
@ -246,7 +248,6 @@ export default NextAuth({
|
||||||
if (account.provider === "saml") {
|
if (account.provider === "saml") {
|
||||||
idP = IdentityProvider.SAML;
|
idP = IdentityProvider.SAML;
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingUser = await prisma.user.findFirst({
|
const existingUser = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
AND: [
|
AND: [
|
||||||
|
@ -292,6 +293,7 @@ export default NextAuth({
|
||||||
},
|
},
|
||||||
async signIn(params) {
|
async signIn(params) {
|
||||||
const { user, account, profile } = params;
|
const { user, account, profile } = params;
|
||||||
|
|
||||||
if (account.provider === "email") {
|
if (account.provider === "email") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -323,10 +325,20 @@ export default NextAuth({
|
||||||
if (!user.email_verified) {
|
if (!user.email_verified) {
|
||||||
return "/auth/error?error=unverified-email";
|
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({
|
const existingUser = await prisma.user.findFirst({
|
||||||
|
include: {
|
||||||
|
accounts: {
|
||||||
where: {
|
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
|
// In this case there's an existing user and their email address
|
||||||
// hasn't changed since they last logged in.
|
// hasn't changed since they last logged in.
|
||||||
if (existingUser.email === user.email) {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -394,7 +417,7 @@ export default NextAuth({
|
||||||
return "/auth/error?error=use-identity-login";
|
return "/auth/error?error=use-identity-login";
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.user.create({
|
const newUser = await prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
// Slugify the incoming name and append a few random characters to
|
// Slugify the incoming name and append a few random characters to
|
||||||
// prevent conflicts for users with the same name.
|
// prevent conflicts for users with the same name.
|
||||||
|
@ -406,6 +429,8 @@ export default NextAuth({
|
||||||
identityProviderId: user.id as string,
|
identityProviderId: user.id as string,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const linkAccountNewUserData = { ...account, userId: newUser.id };
|
||||||
|
await calcomAdapter.linkAccount(linkAccountNewUserData);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { CheckIcon } from "@heroicons/react/outline";
|
import { CheckIcon } from "@heroicons/react/outline";
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { useSession, signOut } from "next-auth/react";
|
import { useSession, signOut } from "next-auth/react";
|
||||||
|
import { getCookieParser } from "next/dist/server/api-utils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
@ -51,6 +52,11 @@ export default function Logout(props: Props) {
|
||||||
|
|
||||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
const ssr = await ssrInit(context);
|
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 {
|
return {
|
||||||
props: {
|
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")
|
impersonatedUsers Impersonations[] @relation("impersonated_user")
|
||||||
impersonatedBy Impersonations[] @relation("impersonated_by_user")
|
impersonatedBy Impersonations[] @relation("impersonated_by_user")
|
||||||
apiKeys ApiKey[]
|
apiKeys ApiKey[]
|
||||||
|
accounts Account[]
|
||||||
|
sessions Session[]
|
||||||
|
|
||||||
@@map(name: "users")
|
@@map(name: "users")
|
||||||
}
|
}
|
||||||
|
@ -403,3 +405,30 @@ model ApiKey {
|
||||||
hashedKey String @unique()
|
hashedKey String @unique()
|
||||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
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