Skip to content

Commit

Permalink
Merge pull request #28 from ava-labs/development
Browse files Browse the repository at this point in the history
Display V2 ReCaptcha when V3 fails
  • Loading branch information
rajranjan0608 authored Jun 22, 2022
2 parents 53227f9 + c9a386b commit ee24652
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 70 deletions.
118 changes: 69 additions & 49 deletions client/src/components/FaucetForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { AxiosResponse } from 'axios'
const FaucetForm = (props: any) => {
const [chain, setChain] = useState<number | null>(null)
const [token, setToken] = useState<number | null>(null)
const [widgetID, setwidgetID] = useState<string | undefined>(undefined)
const [isV2, setIsV2] = useState<boolean>(false)
const [chainConfigs, setChainConfigs] = useState<any>([])
const [inputAddress, setInputAddress] = useState<string>("")
const [address, setAddress] = useState<string | null>(null)
Expand All @@ -28,7 +30,13 @@ const FaucetForm = (props: any) => {
message: null
})

const recaptcha: ReCaptcha = new ReCaptcha(props.config.SITE_KEY, props.config.ACTION)
const recaptcha: ReCaptcha = new ReCaptcha(
props.config.SITE_KEY,
props.config.ACTION,
props.config.V2_SITE_KEY,
setwidgetID,
widgetID
)

// Update chain configs
useEffect(() => {
Expand Down Expand Up @@ -250,9 +258,9 @@ const FaucetForm = (props: any) => {
}
}

async function getCaptchaToken(): Promise<string> {
const token: string = await recaptcha.getToken()
return token
async function getCaptchaToken(): Promise<{token?:string, v2Token?: string}> {
const { token, v2Token } = await recaptcha.getToken(isV2)
return { token, v2Token }
}

function updateChain(option: any): void {
Expand Down Expand Up @@ -281,13 +289,14 @@ const FaucetForm = (props: any) => {
try {
setIsLoading(true)

const token = await getCaptchaToken()
const { token, v2Token } = await getCaptchaToken()

let { chain, erc20 } = getChainParams()

const response = await props.axios.post(props.config.api.sendToken, {
address,
token,
v2Token,
chain,
erc20
})
Expand All @@ -296,6 +305,13 @@ const FaucetForm = (props: any) => {
data = err?.response?.data || err
}

if(typeof data?.message == "string") {
if(data.message.includes("Captcha verification failed")) {
setIsV2(true)
!isV2 && recaptcha?.loadV2Captcha(props.config.V2_SITE_KEY);
}
}

setSendTokenResponse({
txHash: data?.txHash,
message: data?.message
Expand Down Expand Up @@ -405,7 +421,13 @@ const FaucetForm = (props: any) => {
</div>
)

const resetRecaptcha = (): void => {
setIsV2(false)
recaptcha.resetV2Captcha()
}

const back = (): void => {
resetRecaptcha()
setSendTokenResponse({
txHash: null,
message: null
Expand Down Expand Up @@ -457,58 +479,56 @@ const FaucetForm = (props: any) => {

<br/>

{
!sendTokenResponse.txHash
?
<div>
<p className='rate-limit-text'>
Drops are limited to
<span>
{chainConfigs[token!]?.RATELIMIT?.MAX_LIMIT} request in {toString(chainConfigs[token!]?.RATELIMIT?.WINDOW_SIZE)}.
</span>
</p>
<div style={{ display: sendTokenResponse?.txHash ? "none" : "block" }}>
<p className='rate-limit-text'>
Drops are limited to
<span>
{chainConfigs[token!]?.RATELIMIT?.MAX_LIMIT} request in {toString(chainConfigs[token!]?.RATELIMIT?.WINDOW_SIZE)}.
</span>
</p>

<div className='address-input'>
<input placeholder='Hexadecimal Address (0x...)' value={inputAddress || ""} onChange={(e) => updateAddress(e.target.value)} autoFocus/>
</div>
<span className='rate-limit-text' style={{color: "red"}}>{sendTokenResponse?.message}</span>
<div className='address-input'>
<input placeholder='Hexadecimal Address (0x...)' value={inputAddress || ""} onChange={(e) => updateAddress(e.target.value)} autoFocus/>
</div>
<span className='rate-limit-text' style={{color: "red"}}>{sendTokenResponse?.message}</span>

<div className="beta-alert">
<p>This is a testnet faucet. Funds are not real.</p>
</div>
<div className='v2-recaptcha' style={{marginTop: "10px"}}></div>

<button className={shouldAllowSend ? 'send-button' : 'send-button-disabled'} onClick={sendToken}>
{
isLoading
?
<ClipLoader size="20px" speedMultiplier={0.3} color="403F40"/>
:
<span>Request {chainConfigs[token || 0]?.DRIP_AMOUNT / 1e9} {chainConfigs[token || 0]?.TOKEN}</span>
}
</button>
<div className="beta-alert">
<p>This is a testnet faucet. Funds are not real.</p>
</div>
:

<button className={shouldAllowSend ? 'send-button' : 'send-button-disabled'} onClick={sendToken}>
{
isLoading
?
<ClipLoader size="20px" speedMultiplier={0.3} color="403F40"/>
:
<span>Request {chainConfigs[token || 0]?.DRIP_AMOUNT / 1e9} {chainConfigs[token || 0]?.TOKEN}</span>
}
</button>
</div>

<div style={{ display: sendTokenResponse?.txHash ? "block" : "none" }}>
<p className='rate-limit-text'>
{sendTokenResponse?.message}
</p>

<div>
<span className='bold-text'>Transaction ID</span>
<p className='rate-limit-text'>
{sendTokenResponse.message}
<a
target = {'_blank'}
href = {chainConfigs[token!]?.EXPLORER + '/tx/' + sendTokenResponse?.txHash}
rel = "noreferrer"
>
{sendTokenResponse?.txHash}
</a>
</p>

<div>
<span className='bold-text'>Transaction ID</span>
<p className='rate-limit-text'>
<a
target = {'_blank'}
href = {chainConfigs[token!].EXPLORER + '/tx/' + sendTokenResponse.txHash}
rel = "noreferrer"
>
{sendTokenResponse.txHash}
</a>
</p>
</div>

<button className='back-button' onClick={back}>Back</button>
</div>
}

<button className='back-button' onClick={back}>Back</button>
</div>
</div>
</div>

Expand Down
67 changes: 55 additions & 12 deletions client/src/components/ReCaptcha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,69 @@ declare global {

export default class ReCaptcha {
siteKey: string
v2siteKey?: string
action: string
constructor(SITE_KEY: string, ACTION: string) {
loadReCaptcha(SITE_KEY)
widgetID: string | undefined
setWidgetID: any

constructor(SITE_KEY: string, ACTION: string, V2_SITE_KEY: string, setWidgetID: any, widgetID: string | undefined) {
this.loadReCaptcha(SITE_KEY)

this.siteKey = SITE_KEY
this.v2siteKey = V2_SITE_KEY
this.action = ACTION
this.widgetID = widgetID
this.setWidgetID = setWidgetID
}

async getToken(): Promise<string> {
let token = ""
await window.grecaptcha.execute(this.siteKey, {action: this.action})
async getToken(isV2 = false): Promise<{token?: string, v2Token?: string}> {
let token = "", v2Token = ""
!isV2 && await window.grecaptcha.execute(this.siteKey, {action: this.action})
.then((res: string) => {
token = res
})
return token

if(isV2){
v2Token = await window.grecaptcha.getResponse(this.widgetID)
}

return { token, v2Token }
}
}

const loadReCaptcha = (siteKey: string): boolean => {
const script = document.createElement('script')
script.src = `https://www.recaptcha.net/recaptcha/api.js?render=${siteKey}`
document.body.appendChild(script)
return true
resetV2Captcha = () => {
const v2CaptchaContainer = <HTMLElement>document.getElementsByClassName('v2-recaptcha')[0]
if(v2CaptchaContainer) {
if(this.widgetID) {
window.grecaptcha.reset(this.widgetID)
}
v2CaptchaContainer.style.display = "none"
}
}

loadV2Captcha = (v2siteKey: string) => {
const v2CaptchaContainer = document.getElementsByClassName('v2-recaptcha')[0]

if(this.widgetID || this.widgetID == "0") {
const v2CaptchaContainer = <HTMLElement>document.getElementsByClassName('v2-recaptcha')[0]
if(v2CaptchaContainer) {
v2CaptchaContainer.style.display = "block"
}
} else {
const widgetID = window.grecaptcha.render(v2CaptchaContainer, {
'sitekey' : v2siteKey,
'theme': 'dark'
})
this.setWidgetID(widgetID)
}

return true
}

loadReCaptcha = (siteKey: string): boolean => {
const script = document.createElement('script')
script.src = `https://www.recaptcha.net/recaptcha/api.js?render=${siteKey}`

document.body.appendChild(script)
return true
}
}
1 change: 1 addition & 0 deletions client/src/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"apiTimeout": 10000,
"CAPTCHA": {
"siteKey": "6LerTgYgAAAAALa7WiJs3q0pM30PH6JH4oDi_DaK",
"v2siteKey": "6LcPmIYgAAAAADKWHw28ercYsZV7QbDMz_SUeK-Q",
"action": "faucetdrip"
}
}
1 change: 1 addition & 0 deletions client/src/configure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const config = {
faucetAddress: 'faucetAddress'
},
SITE_KEY: configurations.CAPTCHA.siteKey,
V2_SITE_KEY: configurations.CAPTCHA.v2siteKey,
ACTION: configurations.CAPTCHA.action,
banner: configurations.banner
}
59 changes: 51 additions & 8 deletions middlewares/verifyCaptcha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,78 @@ const axios = require('axios')

export class VerifyCaptcha {
secret: string
v2secret?: string
middleware = (req: any, res: any, next: () => void) => this.verifyCaptcha(req, res, next)

constructor(app: any, CAPTCHA_SECRET: string) {
constructor(app: any, CAPTCHA_SECRET: string, V2_CAPTCHA_SECRET?: string) {
if(typeof CAPTCHA_SECRET != "string") {
throw "Captcha Secret should be string"
}
this.secret = CAPTCHA_SECRET
this.v2secret = V2_CAPTCHA_SECRET
}

async shouldAllow(token: string): Promise<boolean> {
const URL = `https://www.google.com/recaptcha/api/siteverify?secret=${this.secret}&response=${token}`
const response = await axios.post(URL)
async verifyV2Token(v2Token: string): Promise<boolean> {
if(v2Token) {
const URL = `https://www.google.com/recaptcha/api/siteverify?secret=${this.v2secret}&response=${v2Token}`
let response

try {
response = await axios.post(URL)
} catch(err: any){
console.log("Recaptcha V2 error:", err?.message)
}

const data = response?.data

if(data?.success) {
return true;
}
}

return false
}

async verifyV3Token(v3Token: string): Promise<boolean> {
const URL = `https://www.google.com/recaptcha/api/siteverify?secret=${this.secret}&response=${v3Token}`
let response

try {
response = await axios.post(URL)
} catch(err: any){
console.log("Recaptcha V3 error:", err?.message)
}

const data = response?.data

if(data?.success) {
if(data?.score > 0.5) {
return true
if(data?.action == 'faucetdrip') {
if(data?.score > 0.5) {
return true
}
}
}

return false
}

async shouldAllow(token: string, v2Token: string): Promise<boolean> {
if(await this.verifyV3Token(token)) {
return true
} else {
if(await this.verifyV2Token(v2Token)) {
return true
}
}
return false
}

async verifyCaptcha(req: any, res: any, next: () => void) {
const shouldAllow = await this.shouldAllow(req?.body?.token)
const shouldAllow = await this.shouldAllow(req?.body?.token, req?.body?.v2Token)
if(shouldAllow) {
next()
} else {
return res.status(400).send({message: "Captcha verification failed! Try refreshing."})
return res.status(400).send({message: "Captcha verification failed! Try solving below."})
}
}
}
2 changes: 1 addition & 1 deletion server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ new RateLimiter(app, [
...erc20tokens
])

const captcha: VerifyCaptcha = new VerifyCaptcha(app, process.env.CAPTCHA_SECRET!)
const captcha: VerifyCaptcha = new VerifyCaptcha(app, process.env.CAPTCHA_SECRET!, process.env.V2_CAPTCHA_SECRET)

let evms = new Map<string, EVMInstanceAndConfig>()

Expand Down

0 comments on commit ee24652

Please sign in to comment.