🛂 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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user