264 lines
7.9 KiB
TypeScript
264 lines
7.9 KiB
TypeScript
import NextAuth, { Account, AuthOptions } from 'next-auth'
|
|
import EmailProvider from 'next-auth/providers/email'
|
|
import GitHubProvider from 'next-auth/providers/github'
|
|
import GitlabProvider from 'next-auth/providers/gitlab'
|
|
import GoogleProvider from 'next-auth/providers/google'
|
|
import FacebookProvider from 'next-auth/providers/facebook'
|
|
import AzureADProvider from 'next-auth/providers/azure-ad'
|
|
import prisma from '@typebot.io/lib/prisma'
|
|
import { Provider } from 'next-auth/providers'
|
|
import { NextApiRequest, NextApiResponse } from 'next'
|
|
import { customAdapter } from '../../../features/auth/api/customAdapter'
|
|
import { User } from '@typebot.io/prisma'
|
|
import { getAtPath, isDefined } from '@typebot.io/lib'
|
|
import { mockedUser } from '@typebot.io/lib/mockedUser'
|
|
import { getNewUserInvitations } from '@/features/auth/helpers/getNewUserInvitations'
|
|
import { sendVerificationRequest } from '@/features/auth/helpers/sendVerificationRequest'
|
|
import { Ratelimit } from '@upstash/ratelimit'
|
|
import { Redis } from '@upstash/redis/nodejs'
|
|
import got from 'got'
|
|
import { env } from '@typebot.io/env'
|
|
import * as Sentry from '@sentry/nextjs'
|
|
|
|
const providers: Provider[] = []
|
|
|
|
let rateLimit: Ratelimit | undefined
|
|
|
|
if (env.UPSTASH_REDIS_REST_URL && env.UPSTASH_REDIS_REST_TOKEN) {
|
|
rateLimit = new Ratelimit({
|
|
redis: Redis.fromEnv(),
|
|
limiter: Ratelimit.slidingWindow(1, '60 s'),
|
|
})
|
|
}
|
|
|
|
if (env.GITHUB_CLIENT_ID && env.GITHUB_CLIENT_SECRET)
|
|
providers.push(
|
|
GitHubProvider({
|
|
clientId: env.GITHUB_CLIENT_ID,
|
|
clientSecret: env.GITHUB_CLIENT_SECRET,
|
|
})
|
|
)
|
|
|
|
if (env.NEXT_PUBLIC_SMTP_FROM && !env.SMTP_AUTH_DISABLED)
|
|
providers.push(
|
|
EmailProvider({
|
|
server: {
|
|
host: env.SMTP_HOST,
|
|
port: env.SMTP_PORT,
|
|
secure: env.SMTP_SECURE,
|
|
auth: {
|
|
user: env.SMTP_USERNAME,
|
|
pass: env.SMTP_PASSWORD,
|
|
},
|
|
},
|
|
from: env.NEXT_PUBLIC_SMTP_FROM,
|
|
sendVerificationRequest,
|
|
})
|
|
)
|
|
|
|
if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET)
|
|
providers.push(
|
|
GoogleProvider({
|
|
clientId: env.GOOGLE_CLIENT_ID,
|
|
clientSecret: env.GOOGLE_CLIENT_SECRET,
|
|
})
|
|
)
|
|
|
|
if (env.FACEBOOK_CLIENT_ID && env.FACEBOOK_CLIENT_SECRET)
|
|
providers.push(
|
|
FacebookProvider({
|
|
clientId: env.FACEBOOK_CLIENT_ID,
|
|
clientSecret: env.FACEBOOK_CLIENT_SECRET,
|
|
})
|
|
)
|
|
|
|
if (env.GITLAB_CLIENT_ID && env.GITLAB_CLIENT_SECRET) {
|
|
const BASE_URL = env.GITLAB_BASE_URL
|
|
providers.push(
|
|
GitlabProvider({
|
|
clientId: env.GITLAB_CLIENT_ID,
|
|
clientSecret: env.GITLAB_CLIENT_SECRET,
|
|
authorization: `${BASE_URL}/oauth/authorize?scope=read_api`,
|
|
token: `${BASE_URL}/oauth/token`,
|
|
userinfo: `${BASE_URL}/api/v4/user`,
|
|
name: env.GITLAB_NAME,
|
|
})
|
|
)
|
|
}
|
|
|
|
if (
|
|
env.AZURE_AD_CLIENT_ID &&
|
|
env.AZURE_AD_CLIENT_SECRET &&
|
|
env.AZURE_AD_TENANT_ID
|
|
) {
|
|
providers.push(
|
|
AzureADProvider({
|
|
clientId: env.AZURE_AD_CLIENT_ID,
|
|
clientSecret: env.AZURE_AD_CLIENT_SECRET,
|
|
tenantId: env.AZURE_AD_TENANT_ID,
|
|
})
|
|
)
|
|
}
|
|
|
|
if (env.CUSTOM_OAUTH_WELL_KNOWN_URL) {
|
|
providers.push({
|
|
id: 'custom-oauth',
|
|
name: env.CUSTOM_OAUTH_NAME,
|
|
type: 'oauth',
|
|
authorization: {
|
|
params: {
|
|
scope: env.CUSTOM_OAUTH_SCOPE,
|
|
},
|
|
},
|
|
clientId: env.CUSTOM_OAUTH_CLIENT_ID,
|
|
clientSecret: env.CUSTOM_OAUTH_CLIENT_SECRET,
|
|
wellKnown: env.CUSTOM_OAUTH_WELL_KNOWN_URL,
|
|
profile(profile) {
|
|
return {
|
|
id: getAtPath(profile, env.CUSTOM_OAUTH_USER_ID_PATH),
|
|
name: getAtPath(profile, env.CUSTOM_OAUTH_USER_NAME_PATH),
|
|
email: getAtPath(profile, env.CUSTOM_OAUTH_USER_EMAIL_PATH),
|
|
image: getAtPath(profile, env.CUSTOM_OAUTH_USER_IMAGE_PATH),
|
|
} as User
|
|
},
|
|
})
|
|
}
|
|
|
|
export const authOptions: AuthOptions = {
|
|
adapter: customAdapter(prisma),
|
|
secret: env.ENCRYPTION_SECRET,
|
|
providers,
|
|
session: {
|
|
strategy: 'database',
|
|
},
|
|
pages: {
|
|
signIn: '/signin',
|
|
newUser: env.NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID ? '/onboarding' : undefined,
|
|
},
|
|
events: {
|
|
signIn({ user }) {
|
|
Sentry.setUser({ id: user.id })
|
|
},
|
|
signOut() {
|
|
Sentry.setUser(null)
|
|
},
|
|
},
|
|
callbacks: {
|
|
session: async ({ session, user }) => {
|
|
const userFromDb = user as User
|
|
await updateLastActivityDate(userFromDb)
|
|
return {
|
|
...session,
|
|
user: userFromDb,
|
|
}
|
|
},
|
|
signIn: async ({ account, user }) => {
|
|
if (!account) return false
|
|
const isNewUser = !('createdAt' in user && isDefined(user.createdAt))
|
|
if (isNewUser && user.email) {
|
|
const { body } = await got.get(
|
|
'https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/master/disposable_email_blocklist.conf'
|
|
)
|
|
const disposableEmailDomains = body.split('\n')
|
|
if (disposableEmailDomains.includes(user.email.split('@')[1]))
|
|
return false
|
|
}
|
|
if (env.DISABLE_SIGNUP && isNewUser && user.email) {
|
|
const { invitations, workspaceInvitations } =
|
|
await getNewUserInvitations(prisma, user.email)
|
|
if (invitations.length === 0 && workspaceInvitations.length === 0)
|
|
return false
|
|
}
|
|
const requiredGroups = getRequiredGroups(account.provider)
|
|
if (requiredGroups.length > 0) {
|
|
const userGroups = await getUserGroups(account)
|
|
return checkHasGroups(userGroups, requiredGroups)
|
|
}
|
|
return true
|
|
},
|
|
},
|
|
}
|
|
|
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|
const isMockingSession =
|
|
req.method === 'GET' &&
|
|
req.url === '/api/auth/session' &&
|
|
env.NEXT_PUBLIC_E2E_TEST
|
|
if (isMockingSession) return res.send({ user: mockedUser })
|
|
const requestIsFromCompanyFirewall = req.method === 'HEAD'
|
|
if (requestIsFromCompanyFirewall) return res.status(200).end()
|
|
|
|
if (
|
|
rateLimit &&
|
|
req.url === '/api/auth/signin/email' &&
|
|
req.method === 'POST'
|
|
) {
|
|
let ip = req.headers['x-real-ip'] as string | undefined
|
|
if (!ip) {
|
|
const forwardedFor = req.headers['x-forwarded-for']
|
|
if (Array.isArray(forwardedFor)) {
|
|
ip = forwardedFor.at(0)
|
|
} else {
|
|
ip = forwardedFor?.split(',').at(0) ?? 'Unknown'
|
|
}
|
|
}
|
|
const { success } = await rateLimit.limit(ip as string)
|
|
if (!success) return res.status(429).json({ error: 'Too many requests' })
|
|
}
|
|
return await NextAuth(req, res, authOptions)
|
|
}
|
|
|
|
const updateLastActivityDate = async (user: User) => {
|
|
const datesAreOnSameDay = (first: Date, second: Date) =>
|
|
first.getFullYear() === second.getFullYear() &&
|
|
first.getMonth() === second.getMonth() &&
|
|
first.getDate() === second.getDate()
|
|
|
|
if (!datesAreOnSameDay(user.lastActivityAt, new Date()))
|
|
await prisma.user.updateMany({
|
|
where: { id: user.id },
|
|
data: { lastActivityAt: new Date() },
|
|
})
|
|
}
|
|
|
|
const getUserGroups = async (account: Account): Promise<string[]> => {
|
|
switch (account.provider) {
|
|
case 'gitlab': {
|
|
const getGitlabGroups = async (
|
|
accessToken: string,
|
|
page = 1
|
|
): Promise<{ full_path: string }[]> => {
|
|
const res = await fetch(
|
|
`${
|
|
env.GITLAB_BASE_URL || 'https://gitlab.com'
|
|
}/api/v4/groups?per_page=100&page=${page}`,
|
|
{ headers: { Authorization: `Bearer ${accessToken}` } }
|
|
)
|
|
const groups: { full_path: string }[] = await res.json()
|
|
const nextPage = parseInt(res.headers.get('X-Next-Page') || '')
|
|
if (nextPage)
|
|
groups.push(...(await getGitlabGroups(accessToken, nextPage)))
|
|
return groups
|
|
}
|
|
const groups = await getGitlabGroups(account.access_token as string)
|
|
return groups.map((group) => group.full_path)
|
|
}
|
|
default:
|
|
return []
|
|
}
|
|
}
|
|
|
|
const getRequiredGroups = (provider: string): string[] => {
|
|
switch (provider) {
|
|
case 'gitlab':
|
|
return env.GITLAB_REQUIRED_GROUPS ?? []
|
|
default:
|
|
return []
|
|
}
|
|
}
|
|
|
|
const checkHasGroups = (userGroups: string[], requiredGroups: string[]) =>
|
|
userGroups?.some((userGroup) => requiredGroups?.includes(userGroup))
|
|
|
|
export default handler
|