🛂 Auto ban IP on suspected bot publishing (#1095)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Enhanced sign-in error handling with specific messages for different
error types.
- Implemented IP-based restrictions for authentication and publishing
actions.

- **Bug Fixes**
- Updated the retrieval of user session information to improve
reliability.

- **Documentation**
- Updated usage instructions for `getServerSession` to reflect the new
authentication options.

- **Refactor**
- Replaced direct usage of `authOptions` with a new function
`getAuthOptions` to dynamically generate authentication options.
- Improved IP address extraction logic to handle various header formats.

- **Chores**
- Added a new `BannedIp` model to the database schema for managing
IP-based restrictions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Baptiste Arnaud
2023-12-11 13:40:07 +01:00
committed by GitHub
parent eedb7145ac
commit fcfbd63443
13 changed files with 171 additions and 38 deletions

View File

@@ -78,17 +78,29 @@ export const SignInForm = ({
redirect: false,
})
if (response?.error) {
showToast({
title: t('auth.signinErrorToast.title'),
description: t('auth.signinErrorToast.description'),
})
if (response.error.includes('ip-banned'))
showToast({
status: 'info',
description:
'Your account has suspicious activity and is being reviewed by our team. Feel free to contact us.',
})
else if (response.error.includes('rate-limited'))
showToast({
status: 'info',
description: t('auth.signinErrorToast.tooManyRequests'),
})
else
showToast({
title: t('auth.signinErrorToast.title'),
description: t('auth.signinErrorToast.description'),
})
} else {
setIsMagicLinkSent(true)
}
} catch {
} catch (e) {
showToast({
status: 'info',
description: t('auth.signinErrorToast.tooManyRequests'),
description: 'An error occured while signing in',
})
}
setAuthLoading(false)

View File

@@ -1,5 +1,5 @@
import prisma from '@typebot.io/lib/prisma'
import { authOptions } from '@/pages/api/auth/[...nextauth]'
import { getAuthOptions } from '@/pages/api/auth/[...nextauth]'
import * as Sentry from '@sentry/nextjs'
import { User } from '@typebot.io/prisma'
import { NextApiRequest, NextApiResponse } from 'next'
@@ -15,7 +15,7 @@ export const getAuthenticatedUser = async (
if (bearerToken) return authenticateByToken(bearerToken)
const user = env.NEXT_PUBLIC_E2E_TEST
? mockedUser
: ((await getServerSession(req, res, authOptions))?.user as
: ((await getServerSession(req, res, getAuthOptions({})))?.user as
| User
| undefined)
if (!user || !('id' in user)) return

View File

@@ -32,6 +32,7 @@ import { parseDefaultPublicId } from '../helpers/parseDefaultPublicId'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { ConfirmModal } from '@/components/ConfirmModal'
import { TextLink } from '@/components/TextLink'
import { useUser } from '@/features/account/hooks/useUser'
type Props = ButtonProps & {
isMoreMenuDisabled?: boolean
@@ -44,6 +45,7 @@ export const PublishButton = ({
const { workspace } = useWorkspace()
const { push, query, pathname } = useRouter()
const { isOpen, onOpen, onClose } = useDisclosure()
const { logOut } = useUser()
const {
isOpen: isNewEngineWarningOpen,
onOpen: onNewEngineWarningOpen,
@@ -69,11 +71,13 @@ export const PublishButton = ({
const { mutate: publishTypebotMutate, isLoading: isPublishing } =
trpc.typebot.publishTypebot.useMutation({
onError: (error) =>
onError: (error) => {
showToast({
title: 'Error while publishing typebot',
description: error.message,
}),
})
if (error.data?.httpStatus === 403) logOut()
},
onSuccess: () => {
refetchPublishedTypebot({
typebotId: typebot?.id as string,

View File

@@ -37,7 +37,7 @@ export const publishTypebot = authenticatedProcedure
message: z.literal('success'),
})
)
.mutation(async ({ input: { typebotId }, ctx: { user } }) => {
.mutation(async ({ input: { typebotId }, ctx: { user, ip } }) => {
const existingTypebot = await prisma.typebot.findFirst({
where: {
id: typebotId,
@@ -87,12 +87,16 @@ export const publishTypebot = authenticatedProcedure
'Radar detected a potential malicious typebot. This bot is being manually reviewed by Fraud Prevention team.',
})
const riskLevel = computeRiskLevel({
name: existingTypebot.name,
groups: parseGroups(existingTypebot.groups, {
typebotVersion: existingTypebot.version,
}),
})
const typebotWasVerified = existingTypebot.riskLevel === -1
const riskLevel = typebotWasVerified
? 0
: computeRiskLevel({
name: existingTypebot.name,
groups: parseGroups(existingTypebot.groups, {
typebotVersion: existingTypebot.version,
}),
})
if (riskLevel > 0 && riskLevel !== existingTypebot.riskLevel) {
if (env.MESSAGE_WEBHOOK_URL && riskLevel !== 100)
@@ -118,6 +122,21 @@ export const publishTypebot = authenticatedProcedure
id: existingTypebot.publishedTypebot.id,
},
})
if (ip) {
const isIpAlreadyBanned = await prisma.bannedIp.findFirst({
where: {
ip,
},
})
if (!isIpAlreadyBanned)
await prisma.bannedIp.create({
data: {
ip,
responsibleTypebotId: existingTypebot.id,
userId: user.id,
},
})
}
throw new TRPCError({
code: 'FORBIDDEN',
message: