2024-08-26 18:48:00 -03:00
import { parseApi } from "../lib/parseApi" ;
import { getRateLimiter } from "../services/rate-limiter" ;
2024-08-12 13:42:09 -04:00
import {
AuthResponse ,
NotificationType ,
RateLimiterMode ,
2024-08-26 18:48:00 -03:00
} from "../types" ;
import { supabase_service } from "../services/supabase" ;
import { withAuth } from "../lib/withAuth" ;
2024-05-14 18:08:31 -03:00
import { RateLimiterRedis } from "rate-limiter-flexible" ;
2024-08-12 13:42:09 -04:00
import { setTraceAttributes } from "@hyperdx/node-opentelemetry" ;
2024-08-26 18:48:00 -03:00
import { sendNotification } from "../services/notification/email_notification" ;
import { Logger } from "../lib/logger" ;
import { redlock } from "../services/redlock" ;
import { getValue } from "../services/redis" ;
import { setValue } from "../services/redis" ;
2024-08-12 13:42:09 -04:00
import { validate } from "uuid" ;
2024-08-22 03:55:40 +02:00
import * as Sentry from "@sentry/node" ;
2024-08-26 19:57:27 -03:00
// const { data, error } = await supabase_service
// .from('api_keys')
// .select(`
// key,
// team_id,
// teams (
// subscriptions (
// price_id
// )
// )
// `)
// .eq('key', normalizedApi)
// .limit(1)
// .single();
2024-08-12 13:37:47 -04:00
function normalizedApiIsUuid ( potentialUuid : string ) : boolean {
// Check if the string is a valid UUID
return validate ( potentialUuid ) ;
}
2024-08-12 13:42:09 -04:00
export async function authenticateUser (
req ,
res ,
mode? : RateLimiterMode
) : Promise < AuthResponse > {
2024-04-21 10:36:48 -07:00
return withAuth ( supaAuthenticateUser ) ( req , res , mode ) ;
}
2024-05-20 13:36:34 -07:00
function setTrace ( team_id : string , api_key : string ) {
try {
setTraceAttributes ( {
team_id ,
2024-08-12 13:42:09 -04:00
api_key ,
2024-05-20 13:36:34 -07:00
} ) ;
} catch ( error ) {
2024-08-22 03:55:40 +02:00
Sentry . captureException ( error ) ;
2024-07-23 17:30:46 -03:00
Logger . error ( ` Error setting trace attributes: ${ error . message } ` ) ;
2024-05-20 13:36:34 -07:00
}
}
2024-08-12 15:07:30 -04:00
async function getKeyAndPriceId ( normalizedApi : string ) : Promise < {
success : boolean ;
teamId? : string ;
priceId? : string ;
error? : string ;
status? : number ;
} > {
const { data , error } = await supabase_service . rpc ( "get_key_and_price_id_2" , {
api_key : normalizedApi ,
} ) ;
if ( error ) {
2024-08-22 03:55:40 +02:00
Sentry . captureException ( error ) ;
2024-08-12 15:07:30 -04:00
Logger . error ( ` RPC ERROR (get_key_and_price_id_2): ${ error . message } ` ) ;
return {
success : false ,
error :
"The server seems overloaded. Please contact hello@firecrawl.com if you aren't sending too many requests at once." ,
status : 500 ,
} ;
}
if ( ! data || data . length === 0 ) {
2024-08-22 14:08:09 +02:00
if ( error ) {
Logger . warn ( ` Error fetching api key: ${ error . message } or data is empty ` ) ;
Sentry . captureException ( error ) ;
}
2024-08-12 15:07:30 -04:00
// TODO: change this error code ?
return {
success : false ,
error : "Unauthorized: Invalid token" ,
status : 401 ,
} ;
} else {
return {
success : true ,
teamId : data [ 0 ] . team_id ,
priceId : data [ 0 ] . price_id ,
} ;
}
}
2024-04-21 10:36:48 -07:00
export async function supaAuthenticateUser (
2024-04-20 16:38:05 -07:00
req ,
res ,
mode? : RateLimiterMode
) : Promise < {
success : boolean ;
team_id? : string ;
error? : string ;
status? : number ;
2024-05-30 14:46:55 -07:00
plan? : string ;
2024-04-20 16:38:05 -07:00
} > {
const authHeader = req . headers . authorization ;
if ( ! authHeader ) {
return { success : false , error : "Unauthorized" , status : 401 } ;
}
const token = authHeader . split ( " " ) [ 1 ] ; // Extract the token from "Bearer <token>"
if ( ! token ) {
return {
success : false ,
error : "Unauthorized: Token missing" ,
status : 401 ,
} ;
}
2024-05-14 18:08:31 -03:00
const incomingIP = ( req . headers [ "x-forwarded-for" ] ||
req . socket . remoteAddress ) as string ;
const iptoken = incomingIP + token ;
let rateLimiter : RateLimiterRedis ;
2024-08-12 13:42:09 -04:00
let subscriptionData : { team_id : string ; plan : string } | null = null ;
2024-05-14 18:08:31 -03:00
let normalizedApi : string ;
2024-08-12 13:42:09 -04:00
let cacheKey = "" ;
2024-08-12 13:37:47 -04:00
let redLockKey = "" ;
2024-08-12 15:07:30 -04:00
const lockTTL = 15000 ; // 10 seconds
2024-08-12 13:37:47 -04:00
let teamId : string | null = null ;
let priceId : string | null = null ;
2024-06-05 13:20:26 -07:00
2024-05-14 18:08:31 -03:00
if ( token == "this_is_just_a_preview_token" ) {
2024-05-17 15:37:47 -03:00
rateLimiter = getRateLimiter ( RateLimiterMode . Preview , token ) ;
2024-08-12 13:37:47 -04:00
teamId = "preview" ;
2024-05-17 15:37:47 -03:00
} else {
2024-05-14 18:08:31 -03:00
normalizedApi = parseApi ( token ) ;
2024-08-12 13:42:09 -04:00
if ( ! normalizedApiIsUuid ( normalizedApi ) ) {
2024-08-12 13:37:47 -04:00
return {
success : false ,
error : "Unauthorized: Invalid token" ,
status : 401 ,
} ;
}
2024-08-12 15:07:30 -04:00
2024-08-12 13:37:47 -04:00
cacheKey = ` api_key: ${ normalizedApi } ` ;
2024-08-12 13:42:09 -04:00
try {
2024-08-12 15:07:30 -04:00
const teamIdPriceId = await getValue ( cacheKey ) ;
if ( teamIdPriceId ) {
const { team_id , price_id } = JSON . parse ( teamIdPriceId ) ;
teamId = team_id ;
priceId = price_id ;
} else {
const {
success ,
teamId : tId ,
priceId : pId ,
error ,
status ,
} = await getKeyAndPriceId ( normalizedApi ) ;
if ( ! success ) {
return { success , error , status } ;
2024-08-12 13:37:47 -04:00
}
2024-08-12 15:07:30 -04:00
teamId = tId ;
priceId = pId ;
await setValue (
cacheKey ,
JSON . stringify ( { team_id : teamId , price_id : priceId } ) ,
2024-08-26 19:57:27 -03:00
60
2024-08-12 15:07:30 -04:00
) ;
2024-08-12 13:37:47 -04:00
}
2024-08-12 13:42:09 -04:00
} catch ( error ) {
2024-08-22 03:55:40 +02:00
Sentry . captureException ( error ) ;
2024-08-21 09:28:20 -03:00
Logger . error ( ` Error with auth function: ${ error } ` ) ;
2024-08-12 15:07:30 -04:00
// const {
// success,
// teamId: tId,
// priceId: pId,
// error: e,
// status,
// } = await getKeyAndPriceId(normalizedApi);
// if (!success) {
// return { success, error: e, status };
// }
// teamId = tId;
// priceId = pId;
// const {
// success,
// teamId: tId,
// priceId: pId,
// error: e,
// status,
// } = await getKeyAndPriceId(normalizedApi);
// if (!success) {
// return { success, error: e, status };
// }
// teamId = tId;
// priceId = pId;
2024-08-12 13:37:47 -04:00
}
2024-05-14 18:08:31 -03:00
2024-05-15 08:40:21 -03:00
// get_key_and_price_id_2 rpc definition:
// create or replace function get_key_and_price_id_2(api_key uuid)
// returns table(key uuid, team_id uuid, price_id text) as $$
// begin
// if api_key is null then
// return query
// select null::uuid as key, null::uuid as team_id, null::text as price_id;
// end if;
// return query
// select ak.key, ak.team_id, s.price_id
// from api_keys ak
// left join subscriptions s on ak.team_id = s.team_id
// where ak.key = api_key;
// end;
// $$ language plpgsql;
2024-05-14 18:08:31 -03:00
2024-08-12 13:37:47 -04:00
const plan = getPlanByPriceId ( priceId ) ;
2024-05-20 13:36:34 -07:00
// HyperDX Logging
2024-08-12 13:37:47 -04:00
setTrace ( teamId , normalizedApi ) ;
2024-05-14 18:08:31 -03:00
subscriptionData = {
2024-08-12 13:37:47 -04:00
team_id : teamId ,
2024-08-12 13:42:09 -04:00
plan : plan ,
} ;
2024-05-25 11:57:49 -04:00
switch ( mode ) {
2024-05-14 18:08:31 -03:00
case RateLimiterMode . Crawl :
2024-08-12 13:42:09 -04:00
rateLimiter = getRateLimiter (
RateLimiterMode . Crawl ,
token ,
subscriptionData . plan
) ;
2024-05-14 18:08:31 -03:00
break ;
case RateLimiterMode . Scrape :
2024-08-12 13:42:09 -04:00
rateLimiter = getRateLimiter (
RateLimiterMode . Scrape ,
token ,
2024-08-26 16:05:11 -03:00
subscriptionData . plan ,
teamId
2024-08-12 13:42:09 -04:00
) ;
2024-05-14 18:08:31 -03:00
break ;
2024-05-30 14:42:32 -07:00
case RateLimiterMode . Search :
2024-08-12 13:42:09 -04:00
rateLimiter = getRateLimiter (
RateLimiterMode . Search ,
token ,
subscriptionData . plan
) ;
2024-05-30 14:42:32 -07:00
break ;
2024-08-27 20:02:39 -03:00
case RateLimiterMode . Map :
rateLimiter = getRateLimiter (
RateLimiterMode . Map ,
token ,
subscriptionData . plan
) ;
break ;
2024-05-14 14:47:36 -07:00
case RateLimiterMode . CrawlStatus :
2024-05-15 08:34:49 -03:00
rateLimiter = getRateLimiter ( RateLimiterMode . CrawlStatus , token ) ;
2024-05-14 14:47:36 -07:00
break ;
2024-08-12 13:42:09 -04:00
2024-05-19 12:45:46 -07:00
case RateLimiterMode . Preview :
rateLimiter = getRateLimiter ( RateLimiterMode . Preview , token ) ;
break ;
2024-05-14 14:47:36 -07:00
default :
2024-05-15 08:34:49 -03:00
rateLimiter = getRateLimiter ( RateLimiterMode . Crawl , token ) ;
2024-05-14 14:47:36 -07:00
break ;
2024-05-14 18:08:31 -03:00
// case RateLimiterMode.Search:
// rateLimiter = await searchRateLimiter(RateLimiterMode.Search, token);
// break;
}
}
2024-08-12 13:42:09 -04:00
const team_endpoint_token =
token === "this_is_just_a_preview_token" ? iptoken : teamId ;
2024-06-05 13:20:26 -07:00
2024-04-20 16:38:05 -07:00
try {
2024-06-05 13:20:26 -07:00
await rateLimiter . consume ( team_endpoint_token ) ;
2024-04-20 16:38:05 -07:00
} catch ( rateLimiterRes ) {
2024-07-23 17:30:46 -03:00
Logger . error ( ` Rate limit exceeded: ${ rateLimiterRes } ` ) ;
2024-05-25 11:57:49 -04:00
const secs = Math . round ( rateLimiterRes . msBeforeNext / 1000 ) || 1 ;
const retryDate = new Date ( Date . now ( ) + rateLimiterRes . msBeforeNext ) ;
2024-06-05 13:20:26 -07:00
// We can only send a rate limit email every 7 days, send notification already has the date in between checking
const startDate = new Date ( ) ;
const endDate = new Date ( ) ;
endDate . setDate ( endDate . getDate ( ) + 7 ) ;
2024-08-12 13:42:09 -04:00
2024-06-07 10:39:11 -07:00
// await sendNotification(team_id, NotificationType.RATE_LIMIT_REACHED, startDate.toISOString(), endDate.toISOString());
2024-08-12 15:07:30 -04:00
// Cache longer for 429s
2024-08-12 13:42:09 -04:00
if ( teamId && priceId && mode !== RateLimiterMode . Preview ) {
await setValue (
cacheKey ,
JSON . stringify ( { team_id : teamId , price_id : priceId } ) ,
2024-08-12 15:07:30 -04:00
60 // 10 seconds, cache for everything
2024-08-12 13:42:09 -04:00
) ;
2024-08-12 13:37:47 -04:00
}
2024-04-20 16:38:05 -07:00
return {
success : false ,
2024-08-21 21:51:54 -03:00
error : ` Rate limit exceeded. Consumed (req/min): ${ rateLimiterRes . consumedPoints } , Remaining (req/min): ${ rateLimiterRes . remainingPoints } . Upgrade your plan at https://firecrawl.dev/pricing for increased rate limits or please retry after ${ secs } s, resets at ${ retryDate } ` ,
2024-04-20 16:38:05 -07:00
status : 429 ,
} ;
}
if (
token === "this_is_just_a_preview_token" &&
2024-08-12 13:42:09 -04:00
( mode === RateLimiterMode . Scrape ||
mode === RateLimiterMode . Preview ||
2024-08-27 20:02:39 -03:00
mode === RateLimiterMode . Map ||
// mode === RateLimiterMode.Crawl ||
2024-08-12 13:42:09 -04:00
mode === RateLimiterMode . Search )
2024-04-20 16:38:05 -07:00
) {
return { success : true , team_id : "preview" } ;
2024-04-26 12:57:49 -07:00
// check the origin of the request and make sure its from firecrawl.dev
// const origin = req.headers.origin;
// if (origin && origin.includes("firecrawl.dev")){
// return { success: true, team_id: "preview" };
// }
// if(process.env.ENV !== "production") {
// return { success: true, team_id: "preview" };
// }
// return { success: false, error: "Unauthorized: Invalid token", status: 401 };
2024-04-20 16:38:05 -07:00
}
// make sure api key is valid, based on the api_keys table in supabase
2024-05-14 18:08:31 -03:00
if ( ! subscriptionData ) {
normalizedApi = parseApi ( token ) ;
const { data , error } = await supabase_service
2024-05-25 11:57:49 -04:00
. from ( "api_keys" )
. select ( "*" )
. eq ( "key" , normalizedApi ) ;
2024-05-14 18:08:31 -03:00
2024-07-29 18:44:18 -04:00
if ( error || ! data || data . length === 0 ) {
2024-08-22 03:55:40 +02:00
if ( error ) {
Sentry . captureException ( error ) ;
2024-08-22 14:08:09 +02:00
Logger . warn ( ` Error fetching api key: ${ error . message } or data is empty ` ) ;
2024-08-22 03:55:40 +02:00
}
2024-05-14 18:08:31 -03:00
return {
success : false ,
error : "Unauthorized: Invalid token" ,
status : 401 ,
} ;
}
subscriptionData = data [ 0 ] ;
2024-04-20 16:38:05 -07:00
}
2024-08-12 13:42:09 -04:00
return {
success : true ,
team_id : subscriptionData.team_id ,
plan : subscriptionData.plan ? ? "" ,
} ;
2024-04-20 16:38:05 -07:00
}
2024-05-14 18:08:31 -03:00
function getPlanByPriceId ( price_id : string ) {
switch ( price_id ) {
2024-05-30 14:31:36 -07:00
case process . env . STRIPE_PRICE_ID_STARTER :
2024-08-12 13:42:09 -04:00
return "starter" ;
2024-05-14 18:08:31 -03:00
case process . env . STRIPE_PRICE_ID_STANDARD :
2024-08-12 13:42:09 -04:00
return "standard" ;
2024-05-14 18:08:31 -03:00
case process . env . STRIPE_PRICE_ID_SCALE :
2024-08-12 13:42:09 -04:00
return "scale" ;
2024-07-30 10:37:33 -03:00
case process . env . STRIPE_PRICE_ID_HOBBY :
case process . env . STRIPE_PRICE_ID_HOBBY_YEARLY :
2024-08-12 13:42:09 -04:00
return "hobby" ;
2024-07-30 10:37:33 -03:00
case process . env . STRIPE_PRICE_ID_STANDARD_NEW :
case process . env . STRIPE_PRICE_ID_STANDARD_NEW_YEARLY :
2024-08-12 13:42:09 -04:00
return "standardnew" ;
2024-07-30 10:37:33 -03:00
case process . env . STRIPE_PRICE_ID_GROWTH :
case process . env . STRIPE_PRICE_ID_GROWTH_YEARLY :
2024-08-12 13:42:09 -04:00
return "growth" ;
2024-08-15 18:37:19 -04:00
case process . env . STRIPE_PRICE_ID_GROWTH_DOUBLE_MONTHLY :
return "growthdouble" ;
2024-05-14 18:08:31 -03:00
default :
2024-08-12 13:42:09 -04:00
return "free" ;
2024-05-14 18:08:31 -03:00
}
2024-08-12 13:42:09 -04:00
}