Files
formbricks-formbricks/packages/lib/authOptions.ts
Piyush Gupta 1af1a92fec feat: granular team roles (#3975)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
Co-authored-by: Johannes <johannes@formbricks.com>
2024-11-08 06:03:14 +00:00

317 lines
10 KiB
TypeScript

import type { IdentityProvider } from "@prisma/client";
import type { NextAuthOptions } from "next-auth";
import AzureAD from "next-auth/providers/azure-ad";
import CredentialsProvider from "next-auth/providers/credentials";
import GitHubProvider from "next-auth/providers/github";
import GoogleProvider from "next-auth/providers/google";
import { prisma } from "@formbricks/database";
import { TUserNotificationSettings } from "@formbricks/types/user";
import { createAccount } from "./account/service";
import { verifyPassword } from "./auth/utils";
import {
AZUREAD_CLIENT_ID,
AZUREAD_CLIENT_SECRET,
AZUREAD_TENANT_ID,
DEFAULT_ORGANIZATION_ID,
DEFAULT_ORGANIZATION_ROLE,
EMAIL_VERIFICATION_DISABLED,
GITHUB_ID,
GITHUB_SECRET,
GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET,
OIDC_CLIENT_ID,
OIDC_CLIENT_SECRET,
OIDC_DISPLAY_NAME,
OIDC_ISSUER,
OIDC_SIGNING_ALGORITHM,
} from "./constants";
import { verifyToken } from "./jwt";
import { createMembership } from "./membership/service";
import { createOrganization, getOrganization } from "./organization/service";
import { createUser, getUserByEmail, updateUser } from "./user/service";
import { findMatchingLocale } from "./utils/locale";
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",
},
},
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("Invalid credentials");
}
if (!user.password) {
throw new Error("Invalid credentials");
}
const isValid = await verifyPassword(credentials.password, user.password);
if (!isValid) {
throw new Error("Invalid credentials");
}
return {
id: user.id,
email: user.email,
emailVerified: user.emailVerified,
imageUrl: user.imageUrl,
};
},
}),
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 updateUser(user.id, { emailVerified: new Date() });
return user;
},
}),
GitHubProvider({
clientId: GITHUB_ID || "",
clientSecret: GITHUB_SECRET || "",
}),
GoogleProvider({
clientId: GOOGLE_CLIENT_ID || "",
clientSecret: GOOGLE_CLIENT_SECRET || "",
allowDangerousEmailAccountLinking: true,
}),
AzureAD({
clientId: AZUREAD_CLIENT_ID || "",
clientSecret: AZUREAD_CLIENT_SECRET || "",
tenantId: AZUREAD_TENANT_ID || "",
}),
{
id: "openid",
name: OIDC_DISPLAY_NAME || "OpenId",
type: "oauth",
clientId: OIDC_CLIENT_ID || "",
clientSecret: OIDC_CLIENT_SECRET || "",
wellKnown: `${OIDC_ISSUER}/.well-known/openid-configuration`,
authorization: { params: { scope: "openid email profile" } },
idToken: true,
client: {
id_token_signed_response_alg: OIDC_SIGNING_ALGORITHM || "RS256",
},
checks: ["pkce", "state"],
profile: (profile) => {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture,
};
},
},
],
callbacks: {
async jwt({ token }) {
const existingUser = await getUserByEmail(token?.email!);
if (!existingUser) {
return token;
}
return {
...token,
profile: { id: existingUser.id },
};
},
async session({ session, token }) {
// @ts-expect-error
session.user.id = token?.id;
// @ts-expect-error
session.user = token.profile;
return session;
},
async signIn({ user, account }: any) {
if (account.provider === "credentials" || account.provider === "token") {
// check if user's email is verified or not
if (!user.emailVerified && !EMAIL_VERIFICATION_DISABLED) {
throw new Error("Email Verification is Pending");
}
return true;
}
if (!user.email || 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 getUserByEmail(user.email);
if (!otherUserWithEmail) {
await updateUser(existingUserWithAccount.id, { email: user.email });
return true;
}
throw new Error(
"Looks like you updated your email somewhere else. A user with this new email exists already."
);
}
// 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 getUserByEmail(user.email);
if (existingUserWithEmail) {
// Sign in the user with the existing account
return true;
}
const userProfile = await createUser({
name: user.name || user.email.split("@")[0],
email: user.email,
emailVerified: new Date(Date.now()),
identityProvider: provider,
identityProviderAccountId: account.providerAccountId,
locale: findMatchingLocale(),
});
// Default organization assignment if env variable is set
if (DEFAULT_ORGANIZATION_ID && DEFAULT_ORGANIZATION_ID.length > 0) {
// check if organization exists
let organization = await getOrganization(DEFAULT_ORGANIZATION_ID);
let isNewOrganization = false;
if (!organization) {
// create organization with id from env
organization = await createOrganization({
id: DEFAULT_ORGANIZATION_ID,
name: userProfile.name + "'s Organization",
});
isNewOrganization = true;
}
const role = isNewOrganization ? "owner" : DEFAULT_ORGANIZATION_ROLE || "manager";
await createMembership(organization.id, userProfile.id, { role: role, accepted: true });
await createAccount({
...account,
userId: userProfile.id,
});
const updatedNotificationSettings: TUserNotificationSettings = {
...userProfile.notificationSettings,
alert: {
...userProfile.notificationSettings?.alert,
},
unsubscribedOrganizationIds: Array.from(
new Set([
...(userProfile.notificationSettings?.unsubscribedOrganizationIds || []),
organization.id,
])
),
weeklySummary: {
...userProfile.notificationSettings?.weeklySummary,
},
};
await updateUser(userProfile.id, {
notificationSettings: updatedNotificationSettings,
});
return true;
}
// Without default organization assignment
return true;
}
return true;
},
},
pages: {
signIn: "/auth/login",
signOut: "/auth/logout",
error: "/auth/login", // Error code passed in query string as ?error=
},
};